mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-22 21:09: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/like_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
import 'package:comunic/ui/widgets/network_image_widget.dart';
|
||||||
import 'package:comunic/ui/widgets/survey_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/date_utils.dart';
|
||||||
import 'package:comunic/utils/files_utils.dart';
|
import 'package:comunic/utils/files_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
@ -197,7 +198,9 @@ class _PostTileState extends State<PostTile> {
|
|||||||
|
|
||||||
// Post text
|
// Post text
|
||||||
Container(
|
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