blob: f33ff33ff70868f8aa51b16291a42fc326c485ca [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'ast.dart';
import 'block_parser.dart';
import 'block_syntaxes/block_syntax.dart';
import 'extension_set.dart';
import 'inline_parser.dart';
import 'inline_syntaxes/inline_syntax.dart';
import 'line.dart';
import 'util.dart';
/// Maintains the context needed to parse a Markdown document.
class Document {
final Map<String, LinkReference> linkReferences = {};
/// Footnote ref count, keys are case-sensitive and added by define syntax.
final footnoteReferences = <String, int>{};
/// Footnote labels by appearing order.
///
/// They are case-insensitive and added by ref syntax.
final footnoteLabels = <String>[];
final Resolver? linkResolver;
final Resolver? imageLinkResolver;
final bool encodeHtml;
/// Whether to use default block syntaxes.
final bool withDefaultBlockSyntaxes;
/// Whether to use default inline syntaxes.
///
/// Need to set both [withDefaultInlineSyntaxes] and [encodeHtml] to
/// `false` to disable all inline syntaxes including html encoding syntaxes.
final bool withDefaultInlineSyntaxes;
final _blockSyntaxes = <BlockSyntax>{};
final _inlineSyntaxes = <InlineSyntax>{};
final bool hasCustomInlineSyntaxes;
Iterable<BlockSyntax> get blockSyntaxes => _blockSyntaxes;
Iterable<InlineSyntax> get inlineSyntaxes => _inlineSyntaxes;
Document({
Iterable<BlockSyntax>? blockSyntaxes,
Iterable<InlineSyntax>? inlineSyntaxes,
ExtensionSet? extensionSet,
this.linkResolver,
this.imageLinkResolver,
this.encodeHtml = true,
this.withDefaultBlockSyntaxes = true,
this.withDefaultInlineSyntaxes = true,
}) : hasCustomInlineSyntaxes = (inlineSyntaxes?.isNotEmpty ?? false) ||
(extensionSet?.inlineSyntaxes.isNotEmpty ?? false) {
if (blockSyntaxes != null) {
_blockSyntaxes.addAll(blockSyntaxes);
}
if (inlineSyntaxes != null) {
_inlineSyntaxes.addAll(inlineSyntaxes);
}
if (extensionSet == null) {
if (withDefaultBlockSyntaxes) {
_blockSyntaxes.addAll(ExtensionSet.commonMark.blockSyntaxes);
}
if (withDefaultInlineSyntaxes) {
_inlineSyntaxes.addAll(ExtensionSet.commonMark.inlineSyntaxes);
}
} else {
_blockSyntaxes.addAll(extensionSet.blockSyntaxes);
_inlineSyntaxes.addAll(extensionSet.inlineSyntaxes);
}
}
/// Parses the given [lines] of Markdown to a series of AST nodes.
List<Node> parseLines(List<String> lines) =>
parseLineList(lines.map(Line.new).toList());
/// Parses the given [text] to a series of AST nodes.
List<Node> parse(String text) => parseLineList(text.toLines());
/// Parses the given [lines] of [Line] to a series of AST nodes.
List<Node> parseLineList(List<Line> lines) {
final nodes = BlockParser(lines, this).parseLines();
_parseInlineContent(nodes);
// Do filter after parsing inline as we need ref count.
return _filterFootnotes(nodes);
}
/// Parses the given inline Markdown [text] to a series of AST nodes.
List<Node> parseInline(String text) => InlineParser(text, this).parse();
void _parseInlineContent(List<Node> nodes) {
for (var i = 0; i < nodes.length; i++) {
final node = nodes[i];
if (node is UnparsedContent) {
final inlineNodes = parseInline(node.textContent);
nodes.removeAt(i);
nodes.insertAll(i, inlineNodes);
i += inlineNodes.length - 1;
} else if (node is Element && node.children != null) {
_parseInlineContent(node.children!);
}
}
}
/// Footnotes could be defined in arbitrary positions of a document, we need
/// to distinguish them and put them behind; and every footnote definition
/// may have multiple backrefs, we need to append backrefs for it.
List<Node> _filterFootnotes(List<Node> nodes) {
final footnotes = <Element>[];
final blocks = <Node>[];
for (final node in nodes) {
if (node is Element &&
node.tag == 'li' &&
footnoteReferences.containsKey(node.footnoteLabel)) {
final label = node.footnoteLabel;
var count = 0;
if (label != null && (count = footnoteReferences[label] ?? 0) > 0) {
footnotes.add(node);
final children = node.children;
if (children != null) {
_appendBackref(children, Uri.encodeComponent(label), count);
}
}
} else {
blocks.add(node);
}
}
if (footnotes.isNotEmpty) {
// Sort footnotes by appearing order.
final ordinal = {
for (var i = 0; i < footnoteLabels.length; i++)
'fn-${footnoteLabels[i]}': i,
};
footnotes.sort((l, r) {
final idl = l.attributes['id']?.toLowerCase() ?? '';
final idr = r.attributes['id']?.toLowerCase() ?? '';
return (ordinal[idl] ?? 0) - (ordinal[idr] ?? 0);
});
final list = Element('ol', footnotes);
// Ignore GFM attribute: <data-footnotes>.
final section = Element('section', [list])
..attributes['class'] = 'footnotes';
blocks.add(section);
}
return blocks;
}
/// Generate backref nodes, append them to footnote definition's last child.
void _appendBackref(List<Node> children, String ref, int count) {
final refs = [
for (var i = 0; i < count; i++) ...[
Text(' '),
_ElementExt.footnoteAnchor(ref, i)
]
];
if (children.isEmpty) {
children.addAll(refs);
} else {
final last = children.last;
if (last is Element) {
last.children?.addAll(refs);
} else {
children.last = Element('p', [last, ...refs]);
}
}
}
}
extension _ElementExt on Element {
static Element footnoteAnchor(String ref, int i) {
final num = '${i + 1}';
final suffix = i > 0 ? '-$num' : '';
final e = Element.empty('tag');
e.match;
return Element('a', [
Text('\u21a9'),
if (i > 0)
Element('sup', [Text(num)])..attributes['class'] = 'footnote-ref',
])
// Ignore GFM's attributes:
// <data-footnote-backref aria-label="Back to content">.
..attributes['href'] = '#fnref-$ref$suffix'
..attributes['class'] = 'footnote-backref';
}
String get match => tag;
}
/// A [link reference
/// definition](https://spec.commonmark.org/0.30/#link-reference-definitions).
class LinkReference {
/// The [link label](https://spec.commonmark.org/0.30/#link-label).
///
/// Temporarily, this class is also being used to represent the link data for
/// an inline link (the destination and title), but this should change before
/// the package is released.
final String label;
/// The [link destination](https://spec.commonmark.org/0.30/#link-destination).
final String destination;
/// The [link title](https://spec.commonmark.org/0.30/#link-title).
final String? title;
/// Construct a new [LinkReference], with all necessary fields.
///
/// If the parsed link reference definition does not include a title, use
/// `null` for the [title] parameter.
LinkReference(this.label, this.destination, this.title);
}