-
-
Notifications
You must be signed in to change notification settings - Fork 951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Support custom attributes syntax to allow for multiple styles in the text rendering pipeline #3519
feat: Support custom attributes syntax to allow for multiple styles in the text rendering pipeline #3519
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import 'package:flame/src/text/nodes/inline_text_node.dart'; | ||
import 'package:flame/text.dart'; | ||
|
||
/// An [InlineTextNode] representing a span of text with a custom style applied. | ||
class CustomInlineTextNode extends InlineTextNode { | ||
final String styleName; | ||
|
||
CustomInlineTextNode(this.child, {required this.styleName}); | ||
|
||
CustomInlineTextNode.simple(String text, {required this.styleName}) | ||
: child = PlainTextNode(text); | ||
|
||
final InlineTextNode child; | ||
|
||
@override | ||
void fillStyles(DocumentStyle stylesheet, InlineTextStyle parentTextStyle) { | ||
style = FlameTextStyle.merge( | ||
parentTextStyle, | ||
stylesheet.getCustomStyle(styleName), | ||
) ?? | ||
stylesheet.text; | ||
child.fillStyles(stylesheet, style); | ||
} | ||
|
||
@override | ||
TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import 'package:markdown/markdown.dart'; | ||
|
||
// cSpell:ignore charcode.dart (file name from another package) | ||
// NOTE: values obtained from file `charcode.dart` from the markdown package | ||
class _Chars { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this even need a class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No but I thought it was more organized to group these together than let them laying around |
||
/// Character `[`. | ||
static const int leftBracket = 0x5B; | ||
|
||
/// Character `{`. | ||
static const int leftBrace = 0x7B; | ||
|
||
/// Character `}`. | ||
static const int rightBrace = 0x7D; | ||
} | ||
|
||
/// Allows for a toned-down version of custom attributes extension for markdown, | ||
/// inspired by the markdown-it-attrs package. | ||
/// | ||
/// This allows users to specify a custom class name to a span of text: | ||
/// | ||
/// ```markdown | ||
/// [This is a custom class]{.my-custom-class} | ||
/// This word will be [red]{.red} and this one will be [blue]{.blue}. | ||
/// ``` | ||
/// | ||
/// This is based on the standard Link markdown parser (which matches the | ||
/// `[text](url)` and `[text][ref]` syntaxes). | ||
class CustomAttributeSyntax extends LinkSyntax { | ||
/// Creates a new custom attribute syntax. | ||
CustomAttributeSyntax() | ||
: super( | ||
pattern: r'\[', | ||
startCharacter: _Chars.leftBracket, | ||
); | ||
|
||
@override | ||
Iterable<Node>? close( | ||
InlineParser parser, | ||
covariant SimpleDelimiter opener, | ||
Delimiter? closer, { | ||
required List<Node> Function() getChildren, | ||
String? tag, | ||
}) { | ||
final text = parser.source.substring(opener.endPos, parser.pos); | ||
|
||
// The current character is the `]` that closed the span text. | ||
// The next character must be a `{`: | ||
parser.advanceBy(1); | ||
if (parser.isDone) { | ||
return null; // not valid syntax - skip | ||
} | ||
final char = parser.charAt(parser.pos); | ||
if (char != _Chars.leftBrace) { | ||
return null; // not valid syntax - skip | ||
} | ||
|
||
final attributes = _parseAttributes(parser) ?? {}; | ||
final node = _createNode(text, attributes, getChildren: getChildren); | ||
return [node]; | ||
} | ||
|
||
/// Create this node represented by a span with custom attributes. | ||
Node _createNode( | ||
String text, | ||
Map<String, String> attributes, { | ||
required List<Node> Function() getChildren, | ||
}) { | ||
final children = getChildren(); | ||
final element = Element('span', children); | ||
for (final attr in attributes.entries) { | ||
element.attributes[attr.key] = attr.value; | ||
} | ||
return element; | ||
} | ||
|
||
/// At this point, we have parsed a custom tag opening `[`, and then a | ||
/// matching closing `]`, and now [parser] is pointing at an opening `{`. | ||
Map<String, String>? _parseAttributes(InlineParser parser) { | ||
// Start walking to the character just after the opening `{`. | ||
parser.advanceBy(1); | ||
|
||
final buffer = StringBuffer(); | ||
|
||
while (true) { | ||
final char = parser.charAt(parser.pos); | ||
if (char == _Chars.rightBrace) { | ||
final attributes = buffer.toString(); | ||
return _parseAttributeList(attributes); | ||
} | ||
|
||
buffer.writeCharCode(char); | ||
parser.advanceBy(1); | ||
if (parser.isDone) { | ||
return null; // not valid syntax - skip | ||
} | ||
} | ||
} | ||
|
||
Map<String, String>? _parseAttributeList(String attributes) { | ||
// Currently we only support one attribute being the class name. | ||
// More support can be added in the future following the syntax from | ||
// the markdown-it library. | ||
final regex = RegExp(r'\.([a-zA-Z0-9_-]+)'); // matches `.class-name` | ||
final content = attributes.trim(); | ||
final className = regex.firstMatch(content)?.group(1); | ||
if (className == null) { | ||
return null; // not valid syntax - skip | ||
} | ||
return {'class': className}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,7 +51,20 @@ class FlameMarkdown { | |
.map(_castCheck<InlineTextNode>) | ||
.toList(); | ||
final child = _groupInlineChildren(children); | ||
|
||
final customClassName = element.attributes['class']; | ||
if (customClassName != null) { | ||
if (element.tag != 'span') { | ||
throw Exception( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could consider support for other inline elements, for example |
||
'Invalid markdown structure: ' | ||
'Only <span> elements can have custom classes', | ||
); | ||
} | ||
return CustomInlineTextNode(child, styleName: customClassName); | ||
} | ||
|
||
return switch (element.tag) { | ||
'span' => child, | ||
'h1' => HeaderNode(child, level: 1), | ||
'h2' => HeaderNode(child, level: 2), | ||
'h3' => HeaderNode(child, level: 3), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sadly we cannot access the file as it is not exported. tbh I do prefer my naming schema though :P