| // Copyright (c) 2012, 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 'dart:convert'; |
| |
| import 'assets/case_folding.dart'; |
| import 'assets/html_entities.dart'; |
| import 'charcode.dart'; |
| import 'patterns.dart'; |
| |
| /// One or more whitespace, for compressing. |
| final _oneOrMoreWhitespacePattern = RegExp('[ \n\r\t]+'); |
| |
| /// Escapes (`'`), (`"`), (`<`), (`>`) and (`&`) characters. |
| String escapeHtml(String html) => const HtmlEscape(HtmlEscapeMode( |
| escapeApos: true, |
| escapeLtGt: true, |
| escapeQuot: true, |
| )).convert(html); |
| |
| /// Escapes (`"`), (`<`) and (`>`) characters. |
| String escapeHtmlAttribute(String text) => |
| const HtmlEscape(HtmlEscapeMode.attribute).convert(text); |
| |
| /// "Normalizes" a link label, according to the [CommonMark spec]. |
| /// |
| /// [CommonMark spec] https://spec.commonmark.org/0.30/#link-label |
| String normalizeLinkLabel(String label) { |
| var text = label.trim().replaceAll(_oneOrMoreWhitespacePattern, ' '); |
| for (var i = 0; i < text.length; i++) { |
| final mapped = caseFoldingMap[text[i]]; |
| if (mapped != null) { |
| text = text.replaceRange(i, i + 1, mapped); |
| } |
| } |
| return text; |
| } |
| |
| /// Normalizes a link destination, including the process of HTML characters |
| /// decoding and percent encoding. |
| // See the description of these examples: |
| // https://spec.commonmark.org/0.30/#example-501 |
| // https://spec.commonmark.org/0.30/#example-502 |
| String normalizeLinkDestination(String destination) { |
| // Decode first, because the destination might have been partly encoded. |
| // For example https://spec.commonmark.org/0.30/#example-502. |
| // With this function, `foo%20bä` will be parsed in the following steps: |
| // 1. foo bä |
| // 2. foo bä |
| // 3. foo%20b%C3%A4 |
| try { |
| destination = Uri.decodeFull(destination); |
| } catch (_) {} |
| return Uri.encodeFull(decodeHtmlCharacters(destination)); |
| } |
| |
| /// Normalizes a link title, including the process of HTML characters decoding |
| /// and HTML characters escaping. |
| // See the description of these examples: |
| // https://spec.commonmark.org/0.30/#example-505 |
| // https://spec.commonmark.org/0.30/#example-506 |
| // https://spec.commonmark.org/0.30/#example-507 |
| // https://spec.commonmark.org/0.30/#example-508 |
| String normalizeLinkTitle(String title) => |
| escapeHtmlAttribute(decodeHtmlCharacters(title)); |
| |
| /// Decodes HTML entity and numeric character references, for example decode |
| /// `#` to `#`. |
| String decodeHtmlCharacters(String input) => |
| input.replaceAllMapped(htmlCharactersPattern, decodeHtmlCharacterFromMatch); |
| |
| /// Decodes HTML entity and numeric character references from the given [match]. |
| String decodeHtmlCharacterFromMatch(Match match) { |
| final text = match.match; |
| final entity = match[1]; |
| final decimalNumber = match[2]; |
| final hexadecimalNumber = match[3]; |
| |
| // Entity references, see |
| // https://spec.commonmark.org/0.30/#entity-references. |
| if (entity != null) { |
| return htmlEntitiesMap[text] ?? text; |
| } |
| |
| // Decimal numeric character references, see |
| // https://spec.commonmark.org/0.30/#decimal-numeric-character-references. |
| else if (decimalNumber != null) { |
| final decimalValue = int.parse(decimalNumber); |
| int hexValue; |
| if (decimalValue < 1114112 && decimalValue > 1) { |
| hexValue = int.parse(decimalValue.toRadixString(16), radix: 16); |
| } else { |
| hexValue = 0xFFFd; |
| } |
| |
| return String.fromCharCode(hexValue); |
| } |
| |
| // Hexadecimal numeric character references, see |
| // https://spec.commonmark.org/0.30/#hexadecimal-numeric-character-references. |
| else if (hexadecimalNumber != null) { |
| var hexValue = int.parse(hexadecimalNumber, radix: 16); |
| if (hexValue > 0x10ffff || hexValue == 0) { |
| hexValue = 0xFFFd; |
| } |
| return String.fromCharCode(hexValue); |
| } |
| |
| return text; |
| } |
| |
| extension MatchExtensions on Match { |
| /// Returns the whole match String |
| String get match => this[0]!; |
| } |
| |
| /// Escapes the ASCII punctuation characters after backslash(`\`). |
| String escapePunctuation(String input) { |
| final buffer = StringBuffer(); |
| |
| for (var i = 0; i < input.length; i++) { |
| if (input.codeUnitAt(i) == $backslash) { |
| final next = i + 1 < input.length ? input[i + 1] : null; |
| if (next != null && asciiPunctuationCharacters.contains(next)) { |
| i++; |
| } |
| } |
| buffer.write(input[i]); |
| } |
| |
| return buffer.toString(); |
| } |