1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-12-27 21:38:51 +00:00
comunicmobile/lib/utils/bbcode_parser.dart
2020-04-16 10:53:09 +02:00

264 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})
: assert(text != null),
_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, {_ElementStyle style}) {
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.body1.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 = List();
_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 != null && 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, this.finalPos});
}