blob: 93ea1e650c725a5b8b56b293d460e670132ff278 [file] [log] [blame]
// 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 'line.dart';
import 'patterns.dart';
/// One or more whitespace, for compressing.
final _oneOrMoreWhitespacePattern = RegExp('[ \n\r\t]+');
/// Escapes (`"`), (`<`), (`>`) and (`&`) characters.
/// Escapes (`'`) if [escapeApos] is `true`.
String escapeHtml(String html, {bool escapeApos = true}) =>
HtmlEscape(HtmlEscapeMode(
escapeApos: escapeApos,
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) {
// Split by url escaping characters
// Concatenate them with unmodified URL-escaping.
// URL-escaping should be left alone inside the destination
// Refer: https://spec.commonmark.org/0.30/#example-502.
final regex = RegExp('%[0-9A-Fa-f]{2}');
return destination.splitMapJoin(
regex,
onMatch: (m) => m.match,
onNonMatch: (e) {
try {
e = Uri.decodeFull(e);
} catch (_) {}
return Uri.encodeFull(decodeHtmlCharacters(e));
},
);
}
/// 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
/// `&#35` 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();
}
extension StringExtensions on String {
/// Calculates the length of indentation a `String` has.
///
// The behavior of tabs: https://spec.commonmark.org/0.30/#tabs
int indentation() {
var length = 0;
for (final char in codeUnits) {
if (char != $space && char != $tab) {
break;
}
length += char == $tab ? 4 - (length % 4) : 1;
}
return length;
}
/// Removes up to [length] characters of leading whitespace.
// The way of handling tabs: https://spec.commonmark.org/0.30/#tabs
DedentedText dedent([int length = 4]) {
final whitespaceMatch = RegExp('^[ \t]{0,$length}').firstMatch(this);
const tabSize = 4;
int? tabRemaining;
var start = 0;
final whitespaces = whitespaceMatch?[0];
if (whitespaces != null) {
var indentLength = 0;
for (; start < whitespaces.length; start++) {
final isTab = whitespaces[start] == '\t';
if (isTab) {
indentLength += tabSize;
tabRemaining = 4;
} else {
indentLength += 1;
}
if (indentLength >= length) {
if (tabRemaining != null) {
tabRemaining = indentLength - length;
}
if (indentLength == length || isTab) {
start += 1;
}
break;
}
if (tabRemaining != null) {
tabRemaining = 0;
}
}
}
return DedentedText(substring(start), tabRemaining);
}
/// Adds [width] of spaces to the beginning of this string.
String prependSpace(int width) => '${" " * width}$this';
/// Whether this string contains only whitespaces.
bool get isBlank => trim().isEmpty;
/// Converts this string to a list of [Line].
List<Line> toLines() => LineSplitter.split(this).map(Line.new).toList();
/// Returns the last character.
String last([int n = 1]) => substring(length - n);
}
/// A class that describes a dedented text.
class DedentedText {
/// The dedented text.
final String text;
/// How many spaces of a tab that remains after part of it has been consumed.
///
/// `null` means we did not read a `tab`.
final int? tabRemaining;
DedentedText(this.text, this.tabRemaining);
}