mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-22 12:59:21 +00:00
Include BBcode parser
This commit is contained in:
parent
ec1732088a
commit
b7be59bc6e
@ -17,6 +17,7 @@ import 'package:comunic/ui/widgets/countdown_widget.dart';
|
||||
import 'package:comunic/ui/widgets/like_widget.dart';
|
||||
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
||||
import 'package:comunic/ui/widgets/survey_widget.dart';
|
||||
import 'package:comunic/ui/widgets/text_widget.dart';
|
||||
import 'package:comunic/utils/date_utils.dart';
|
||||
import 'package:comunic/utils/files_utils.dart';
|
||||
import 'package:comunic/utils/intl_utils.dart';
|
||||
@ -197,7 +198,9 @@ class _PostTileState extends State<PostTile> {
|
||||
|
||||
// Post text
|
||||
Container(
|
||||
child: widget.post.hasContent ? Text(widget.post.content) : null),
|
||||
child: widget.post.hasContent
|
||||
? TextWidget(content: widget.post.content)
|
||||
: null),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
21
lib/ui/widgets/text_widget.dart
Normal file
21
lib/ui/widgets/text_widget.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'package:comunic/utils/bbcode_parser.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Text widget
|
||||
///
|
||||
/// The content passed to this widget is automatically parsed
|
||||
///
|
||||
/// @author Pierre Hubert
|
||||
|
||||
class TextWidget extends StatelessWidget {
|
||||
final String content;
|
||||
|
||||
const TextWidget({Key key, @required this.content})
|
||||
: assert(content != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BBCodeParsedWidget(text: content);
|
||||
}
|
||||
}
|
264
lib/utils/bbcode_parser.dart
Normal file
264
lib/utils/bbcode_parser.dart
Normal file
@ -0,0 +1,264 @@
|
||||
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<TextSpan> 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(
|
||||
text: text,
|
||||
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});
|
||||
}
|
Loading…
Reference in New Issue
Block a user