mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-24 13:59:22 +00:00
263 lines
6.6 KiB
Dart
263 lines
6.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// BBCode parser
|
|
///
|
|
/// @author Pierre HUBERT
|
|
|
|
/// This callback return null if the text has to be left as is or a TextSpan
|
|
/// if it has been sub parsed...
|
|
typedef ParseCallBack = List<InlineSpan> Function(TextStyle, String?);
|
|
|
|
class BBCodeParsedWidget extends StatelessWidget {
|
|
final _Element? _content;
|
|
final ParseCallBack? parseCallback;
|
|
|
|
BBCodeParsedWidget({required String text, this.parseCallback})
|
|
: _content = _parse(text);
|
|
|
|
_printRecur(_Element el, {int pos = 0}) {
|
|
String str;
|
|
str = "".padLeft(pos, "*");
|
|
if (el.text != null) print(str + el.text!);
|
|
el.children.forEach((f) => _printRecur(f!, pos: pos + 1));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
_printRecur(_content!);
|
|
return RichText(
|
|
text: _content!.toTextSpan(context, parseCallback),
|
|
);
|
|
}
|
|
|
|
/// Initialize parsing
|
|
static _Element? _parse(String text) {
|
|
try {
|
|
return _parseRecur(
|
|
text: text,
|
|
style: _ElementStyle.empty(),
|
|
pos: 0,
|
|
).el;
|
|
} catch (e) {
|
|
print("BBCode parse error: " + e.toString());
|
|
print("Could not parse text!");
|
|
return _Element(text: text, style: _ElementStyle.empty());
|
|
}
|
|
}
|
|
|
|
/// Recursive parsing
|
|
static _ElementRecur _parseRecur({
|
|
required String text,
|
|
required _ElementStyle style,
|
|
required int pos,
|
|
String? parentTag,
|
|
}) {
|
|
_Element el = _Element(style: style.clone());
|
|
|
|
int? lastBeginPos = pos;
|
|
int childNumber = 0;
|
|
bool stop = false;
|
|
while (!stop && pos < text.length) {
|
|
//Go to next stop
|
|
while (!stop && pos < text.length) {
|
|
if (text[pos] == '[') break;
|
|
pos++;
|
|
}
|
|
|
|
//Check for text with default style to apply
|
|
if (lastBeginPos != pos)
|
|
el.children.add(_Element(
|
|
style: style.clone(), text: text.substring(lastBeginPos!, pos)));
|
|
|
|
//Check if the [ tag is alone
|
|
if (pos == text.length)
|
|
break;
|
|
else if (!text.contains("]", pos) ||
|
|
(text.contains("[", pos + 1) &&
|
|
text.indexOf("]", pos) > text.indexOf("[", pos + 1)))
|
|
el.children.add(_Element(style: style.clone(), text: "["));
|
|
|
|
//Check if we have to stop recursion
|
|
else if (text[pos + 1] == "/") {
|
|
pos = text.indexOf("]", pos);
|
|
break;
|
|
}
|
|
|
|
//Check if we have to enter recursion
|
|
else {
|
|
// Prepare tag detection
|
|
final closeBrace = text.indexOf("]", pos);
|
|
String tag = text.substring(pos + 1, closeBrace);
|
|
String? arg;
|
|
final newStyle = style.clone();
|
|
|
|
//Check for argument
|
|
if (tag.contains("=")) {
|
|
final s = tag.split("=");
|
|
tag = s[0];
|
|
arg = s[1];
|
|
}
|
|
|
|
if (tag.length == 0) throw "This BBCode is invalid!";
|
|
|
|
_preParseTag(tag, newStyle, arg);
|
|
|
|
final subParse = _parseRecur(
|
|
text: text,
|
|
pos: closeBrace + 1,
|
|
style: newStyle,
|
|
parentTag: tag,
|
|
);
|
|
|
|
pos = subParse.finalPos;
|
|
_postParseTag(tag, subParse.el,
|
|
arg: arg, parentTag: parentTag, childNumber: childNumber);
|
|
|
|
el.children.add(subParse.el);
|
|
}
|
|
|
|
pos++;
|
|
lastBeginPos = pos;
|
|
childNumber++;
|
|
}
|
|
|
|
return _ElementRecur(el: el, finalPos: pos);
|
|
}
|
|
|
|
/// Pre-parse tag
|
|
static void _preParseTag(String tag, _ElementStyle style, [String? arg]) {
|
|
switch (tag) {
|
|
// Bold
|
|
case "b":
|
|
style.fontWeight = FontWeight.bold;
|
|
break;
|
|
|
|
// Italic
|
|
case "i":
|
|
style.fontStyle = FontStyle.italic;
|
|
break;
|
|
|
|
// Underline
|
|
case "u":
|
|
style.decoration = TextDecoration.underline;
|
|
break;
|
|
|
|
// Strike through
|
|
case "s":
|
|
style.decoration = TextDecoration.lineThrough;
|
|
break;
|
|
|
|
// Color
|
|
case "color":
|
|
assert(arg != null);
|
|
style.color = Color.fromARGB(
|
|
255,
|
|
int.tryParse(arg!.substring(1, 3), radix: 16)!,
|
|
int.tryParse(arg.substring(3, 5), radix: 16)!,
|
|
int.tryParse(arg.substring(5, 7), radix: 16)!,
|
|
);
|
|
break;
|
|
|
|
// Not found
|
|
default:
|
|
print("Tag " + tag + " not understood!");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// Post-parse tag
|
|
static void _postParseTag(String tag, _Element? el,
|
|
{String? arg, String? parentTag, int? childNumber}) {
|
|
// List container
|
|
if (tag == "ul" || tag == "ol")
|
|
el!.children.insert(0, _Element(style: el.style, text: "\n"));
|
|
|
|
// List children
|
|
if (tag == "li") {
|
|
el!.children.add(_Element(style: el.style, text: "\n"));
|
|
if (parentTag == "ol")
|
|
el.children.insert(
|
|
0, _Element(style: el.style, text: " ${childNumber! + 1}. "));
|
|
else
|
|
el.children.insert(0, _Element(style: el.style, text: " \u2022 "));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An element's style
|
|
class _ElementStyle {
|
|
TextDecoration? decoration;
|
|
FontWeight? fontWeight;
|
|
FontStyle? fontStyle;
|
|
Color? color;
|
|
|
|
/// Generate an empty style
|
|
_ElementStyle.empty();
|
|
|
|
/// Construct an instance of this element
|
|
_ElementStyle(
|
|
{required this.decoration,
|
|
required this.fontWeight,
|
|
required this.fontStyle,
|
|
required this.color});
|
|
|
|
/// Clone this style
|
|
_ElementStyle clone() {
|
|
return _ElementStyle(
|
|
decoration: decoration,
|
|
fontWeight: fontWeight,
|
|
fontStyle: fontStyle,
|
|
color: color);
|
|
}
|
|
|
|
/// Generate corresponding TextStyle
|
|
TextStyle toTextStyle(BuildContext context) {
|
|
return Theme.of(context).textTheme.bodyText2!.copyWith(
|
|
decoration: decoration,
|
|
fontWeight: fontWeight,
|
|
fontStyle: fontStyle,
|
|
color: color);
|
|
}
|
|
}
|
|
|
|
/// An element
|
|
class _Element {
|
|
/// Note : if text is not null, children must be empty !!!
|
|
String? text;
|
|
final _ElementStyle style;
|
|
final List<_Element?> children = [];
|
|
|
|
_Element({required this.style, this.text});
|
|
|
|
/// Turn this element into a TextSpan
|
|
TextSpan toTextSpan(BuildContext context, ParseCallBack? parseCallback) {
|
|
assert(text == null || children.length == 0);
|
|
|
|
final generatedStyle = this.style.toTextStyle(context);
|
|
|
|
if (parseCallback != null && text != null) {
|
|
final parsed = parseCallback(generatedStyle, text);
|
|
|
|
if (parsed.length > 0)
|
|
return TextSpan(
|
|
style: generatedStyle,
|
|
children: parsed,
|
|
);
|
|
}
|
|
|
|
return TextSpan(
|
|
text: text,
|
|
style: generatedStyle,
|
|
children:
|
|
children.map((f) => f!.toTextSpan(context, parseCallback)).toList(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ElementRecur {
|
|
final _Element? el;
|
|
final int finalPos;
|
|
|
|
const _ElementRecur({this.el, required this.finalPos});
|
|
}
|