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 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}); }