Added an optional classifier callback to markdownToHtml to classify source found in github style triple backtick code blocks. Added Dart classifier built on analyzer_experimental based on original dartdoc classifier. Added dart handler to default classifier impl.
diff --git a/pkgs/markdown/lib/markdown.dart b/pkgs/markdown/lib/markdown.dart index ba355eb..0bcc777 100644 --- a/pkgs/markdown/lib/markdown.dart +++ b/pkgs/markdown/lib/markdown.dart
@@ -5,7 +5,7 @@ /// Parses text in a markdown-like format and renders to HTML. library markdown; -//import 'classify.dart'; +import 'src/classify/dart.dart'; // TODO(rnystrom): Use "package:" URL (#4968). part 'src/markdown/ast.dart'; @@ -13,9 +13,11 @@ part 'src/markdown/html_renderer.dart'; part 'src/markdown/inline_parser.dart'; +typedef String ClassifierFunction(String syntax, String source); + /// Converts the given string of markdown to HTML. -String markdownToHtml(String markdown) { - final document = new Document(); +String markdownToHtml(String markdown, [ClassifierFunction classifier]) { + final document = new Document(classifier); // Replace windows line endings with unix line endings, and split. final lines = markdown.replaceAll('\r\n','\n').split('\n'); @@ -40,10 +42,17 @@ /// Maintains the context needed to parse a markdown document. class Document { final Map<String, Link> refLinks; - - Document() + final ClassifierFunction classifier; + + Document(this.classifier) : refLinks = <String, Link>{}; - + + String classify(String syntax, String source) { + if (syntax == 'dart') return classifyDart(source); + if (classifier == null) return source; + return classifier(syntax, source); + } + parseRefLinks(List<String> lines) { // This is a hideous regex. It matches: // [id]: http:foo.com "some title"
diff --git a/pkgs/markdown/lib/src/classify/dart.dart b/pkgs/markdown/lib/src/classify/dart.dart new file mode 100644 index 0000000..2b67f9c --- /dev/null +++ b/pkgs/markdown/lib/src/classify/dart.dart
@@ -0,0 +1,173 @@ +library classify; + +import 'package:analyzer_experimental/src/generated/java_core.dart'; +import 'package:analyzer_experimental/src/generated/scanner.dart'; + +class Classification { + static const NONE = ""; + static const ERROR = "e"; + static const COMMENT = "c"; + static const IDENTIFIER = "i"; + static const KEYWORD = "k"; + static const OPERATOR = "o"; + static const STRING = "s"; + static const NUMBER = "n"; + static const PUNCTUATION = "p"; + static const TYPE_IDENTIFIER = "t"; + static const SPECIAL_IDENTIFIER = "r"; + static const ARROW_OPERATOR = "a"; + static const STRING_INTERPOLATION = 'si'; +} + +String classifyDart(String src) { + var scanner = new StringScanner(null, src, null); + var token = scanner.tokenize(); + var out = new StringBuffer(); + var pos = 0; + while (token.type != TokenType.EOF) { + // If not a token and not whitespace assume comment. + var comment = src.slice(pos, token.offset); + if (comment.trim().length > 0) out.add('<span class="${Classification.COMMENT}">$comment</span>'); + else out.add(comment); + pos = token.end; + + var inString = (token.type == TokenType.STRING + || token.type == TokenType.STRING_INTERPOLATION_EXPRESSION + || token.type == TokenType.STRING_INTERPOLATION_IDENTIFIER); + var stringClass = inString ? ' ${Classification.STRING_INTERPOLATION}' : ''; + var kind = classify(token); + out.add('<span class="$kind$stringClass">$token</span>'); + token = token.next; + } + return out.toString(); +} + +Map createTokenMap() { + var map = new Map(); + [ TokenType.OPEN_PAREN, + TokenType.CLOSE_PAREN, + TokenType.OPEN_CURLY_BRACKET, + TokenType.CLOSE_CURLY_BRACKET, + TokenType.OPEN_SQUARE_BRACKET, + TokenType.OPEN_SQUARE_BRACKET, + TokenType.COLON, + TokenType.SEMICOLON, + TokenType.COMMA, + TokenType.PERIOD, + TokenType.PERIOD_PERIOD + ].forEach((t) => map[t] = Classification.PUNCTUATION); + + [ TokenType.INT, + TokenType.HEXADECIMAL, + TokenType.DOUBLE + ].forEach((t) => map[t] = Classification.NUMBER); + + [ TokenType.STRING, + TokenType.STRING_INTERPOLATION_IDENTIFIER, + TokenType.STRING_INTERPOLATION_EXPRESSION, + TokenType.DOUBLE + ].forEach((t) => map[t] = Classification.STRING); + + [ TokenType.PLUS_PLUS, + TokenType.MINUS_MINUS, + TokenType.TILDE, + TokenType.BANG, + TokenType.EQ, + TokenType.BAR_EQ, + TokenType.CARET_EQ, + TokenType.AMPERSAND_EQ, + TokenType.LT_LT_EQ, + TokenType.GT_GT_EQ, + TokenType.PLUS_EQ, + TokenType.MINUS_EQ, + TokenType.STAR_EQ, + TokenType.SLASH_EQ, + TokenType.TILDE_SLASH_EQ, + TokenType.PERCENT_EQ, + TokenType.QUESTION, + TokenType.BAR_BAR, + TokenType.AMPERSAND_AMPERSAND, + TokenType.BAR, + TokenType.CARET, + TokenType.AMPERSAND, + TokenType.LT_LT, + TokenType.GT_GT, + TokenType.PLUS, + TokenType.MINUS, + TokenType.STAR, + TokenType.SLASH, + TokenType.TILDE_SLASH, + TokenType.PERCENT, + TokenType.EQ_EQ, + TokenType.BANG_EQ, + TokenType.LT, + TokenType.GT, + TokenType.LT_EQ, + TokenType.GT_EQ, + TokenType.INDEX, + TokenType.INDEX_EQ, + ].forEach((t) => map[t] = Classification.OPERATOR); + + // => is so awesome it is in a class of its own. + map[TokenType.FUNCTION] = Classification.ARROW_OPERATOR; + + map[TokenType.IDENTIFIER] = Classification.IDENTIFIER; + map[TokenType.KEYWORD] = Classification.KEYWORD; + map[TokenType.HASH] = Classification.KEYWORD; + + return map; +} +var _tokenMap = createTokenMap(); + +String classify(Token token) { + if (!_tokenMap.containsKey(token.type)) { + return Classification.NONE; + } + var classification = _tokenMap[token.type]; + + // Special case for names that look like types. + if (classification == Classification.IDENTIFIER) { + final text = token.lexeme; + if (_looksLikeType(text) + || text == 'num' + || text == 'bool' + || text == 'int' + || text == 'double') { + return Classification.TYPE_IDENTIFIER; + } + } + + // Color keyword token. Most are colored as keywords. + if (classification == Classification.KEYWORD) { + if (token.lexeme == 'void') { + // Color "void" as a type. + return Classification.TYPE_IDENTIFIER; + } + if (token.lexeme == 'this' || token.lexeme == 'super') { + // Color "this" and "super" as identifiers. + return Classification.SPECIAL_IDENTIFIER; + } + } + + return classification; +} + +bool _looksLikeType(String name) { + // If the name looks like an UppercaseName, assume it's a type. + return _looksLikePublicType(name) || _looksLikePrivateType(name); +} + +bool _looksLikePublicType(String name) { + // If the name looks like an UppercaseName, assume it's a type. + return name.length >= 2 && isUpper(name[0]) && isLower(name[1]); +} + +bool _looksLikePrivateType(String name) { + // If the name looks like an _UppercaseName, assume it's a type. + return (name.length >= 3 && name[0] == '_' && isUpper(name[1]) + && isLower(name[2])); +} + +// These ensure that they don't return "true" if the string only has symbols. +bool isUpper(String s) => s.toLowerCase() != s; +bool isLower(String s) => s.toUpperCase() != s;
diff --git a/pkgs/markdown/lib/src/markdown/block_parser.dart b/pkgs/markdown/lib/src/markdown/block_parser.dart index e1d04ec..b59c795 100644 --- a/pkgs/markdown/lib/src/markdown/block_parser.dart +++ b/pkgs/markdown/lib/src/markdown/block_parser.dart
@@ -267,7 +267,6 @@ Node parse(BlockParser parser) { // Get the syntax identifier, if there is one. var syntax = pattern.firstMatch(parser.current).group(1); - print('syntax: $syntax'); final childLines = parseChildLines(parser); @@ -275,7 +274,7 @@ childLines.add(''); // Escape the code. - final escaped = childLines.join('\n').trim(); + final escaped = parser.document.classify(syntax, childLines.join('\n').trim()); return new Element.text('pre', escaped); }
diff --git a/pkgs/markdown/pubspec.yaml b/pkgs/markdown/pubspec.yaml index f4436ad..1682a0f 100644 --- a/pkgs/markdown/pubspec.yaml +++ b/pkgs/markdown/pubspec.yaml
@@ -5,3 +5,4 @@ homepage: https://github.com/dpeek/dart-markdown dependencies: unittest: any + analyzer_experimental: any