docutils-ts
Version:
Port of the Python Docutils library to TypeScript
643 lines (641 loc) • 28.6 kB
JavaScript
import * as nodes from "../../nodes.js";
import { escape2null, getTrimFootnoteRefSpace, isIterable, splitEscapedWhitespace } from "../../utils.js";
import { matchChars } from "../../utils/punctuationChars.js";
import roleInterface from "./roles.js";
import { ApplicationError } from "../../exceptions.js";
import unescape from "../../utils/unescape.js";
import { fullyNormalizeName } from "../../nodeUtils.js";
const uric = '[-_.!~*\'()[\\];/:@&=+$,%a-zA-Z0-9\\x00]';
// # Delimiter indicating the end of a URI (not part of the URI):
const uriEndDelim = '[>]';
const urilast = '[_~*/=+a-zA-Z0-9]';
const uriEnd = `(?:${urilast}|${uric}(?=${uriEndDelim}))`;
const emailc = '[-_!~*\'{|}/#?^`&=+$%a-zA-Z0-9\\x00]';
const emailPattern = `${emailc}+(?:\\.${emailc}+)*(?<!\x00)@${emailc}+(?:\\.${emailc}*)*%(uriEnd)s`;
// email=re.compile(self.email_pattern % args + '$', re.VERBOSE | re.UNICODE),
function buildRegexp(definition, compile = true) {
const [fakeTuple, name, prefix, suffix, parts] = definition;
let myPrefix = prefix.slice();
let mySuffix = suffix.slice();
const myParts = parts.slice();
let prefixNames = [];
if (Array.isArray(myPrefix)) {
myPrefix.shift();
const pr = myPrefix.shift();
prefixNames = [...myPrefix];
myPrefix = pr;
}
if (mySuffix === undefined) {
throw new Error();
}
let suffixNames = [];
if (Array.isArray(mySuffix)) {
mySuffix.shift();
const sr = mySuffix.shift();
suffixNames = [...mySuffix];
mySuffix = sr;
}
if (!fakeTuple) {
throw new Error();
}
// console.log(`buildRegexp(${name} - ${pi})`);
const partStrings = [];
// console.log(parts);
if (myParts === undefined) {
throw new Error();
}
myParts.shift(); /* Remove 'faketuple' */
const groupNames = [];
myParts.forEach((part) => {
const fakeTuple3 = Array.isArray(part) ? part[0] : undefined;
if (fakeTuple3 === 1) {
const [regexp, subGroupNames] = buildRegexp(part, false);
// @ts-ignore
groupNames.push(null, ...subGroupNames);
partStrings.push(regexp);
}
else if (fakeTuple3 === 2) {
const myPart = part.slice();
myPart.shift();
const regexp = myPart.shift();
partStrings.push(regexp);
groupNames.push(null, ...myPart);
}
else {
partStrings.push(part);
groupNames.push(null);
}
});
const orGroup = partStrings.map(x => `(${x})`).join('|');
const regexp = `${myPrefix}(${orGroup})${mySuffix}`;
// console.log(new RegExp(regexp))
groupNames.splice(0, 0, ...prefixNames, name);
groupNames.push(...suffixNames);
// console.log('groupnames')
// console.log(groupNames);
// console.log(`regexp is ${regexp}`);
if (compile) {
return [new RegExp(regexp), groupNames, regexp];
}
return [regexp, groupNames];
}
/**
* Parse inline markup; call the `parse()` method.
*/
class Inliner {
/**
* Create Inliner instance
*/
constructor(document, logger) {
this.nonWhitespaceAfter = '';
this.simplename = '';
this.parts = [];
this.startStringPrefix = '';
this.endStringSuffix = '';
this.logger = logger;
this.dispatch = {
'*': this.emphasis.bind(this),
'**': this.strong.bind(this),
'``': this.literal.bind(this),
'`': this.interpreted_or_phrase_ref.bind(this),
'_`': this.inline_internal_target.bind(this),
_: this.reference.bind(this),
']_': this.footnote_reference.bind(this),
'|': this.substitution_reference.bind(this),
__: this.anonymous_reference.bind(this),
};
this.document = document;
this.reporter = document.reporter;
this.implicitDispatch = [];
this.nonWhitespaceAfter = '';
this.nonWhitespaceBefore = '(?<!\\s)';
this.nonWhitespaceEscapeBefore = '(?<![\\s\\x00])';
this.nonUnescapedWhitespaceEscapeBefore = '(?<!(?<!\\x00)[\\s\\x00])';
}
inline_internal_target(match, lineno) {
const [before, inlines, remaining, sysmessages, endstring] = this.inline_obj(match, lineno, this.patterns.target, nodes.target);
if (inlines && inlines.length && inlines[0] instanceof nodes.target) {
// assert len(inlines) == 1
const target = inlines[0];
const name = fullyNormalizeName(target.astext());
target.attributes.names.push(name);
this.document.noteExplicitTarget(target, this.parent);
}
return [before, inlines, remaining, sysmessages];
}
substitution_reference(match, lineno) {
const [before, inlines, remaining, sysmessages, endstring] = this.inline_obj(match, lineno, this.patterns.substitution_ref, nodes.substitution_reference);
if (inlines.length === 1) {
const subrefNode = inlines[0];
if (subrefNode instanceof nodes.substitution_reference) {
const subrefText = subrefNode.astext();
this.document.noteSubstitutionRef(subrefNode, subrefText);
if (endstring[endstring.length - 1] === '_') {
const referenceNode = new nodes.reference(`|${subrefText}${endstring}`, '');
if (endstring.endsWith('__')) {
referenceNode.attributes.anonymous = 1;
}
else {
referenceNode.attributes.refname = fullyNormalizeName(subrefText);
this.document.noteRefname(referenceNode);
}
referenceNode.add(subrefNode);
inlines[0] = referenceNode;
}
}
}
return [before, inlines, remaining, sysmessages];
}
footnote_reference(match, lineno) {
const label = match.groups.footnotelabel;
let refname = fullyNormalizeName(label);
const string = match.result.input;
let before = string.substring(0, match.result.index);
const remaining = string.substring(match.result.index + match.result[0].length);
let refnode;
if (match.groups.citationlabel) {
refnode = new nodes.citation_reference(`[${label}]_`, '', [], { refname });
refnode.add(new nodes.Text(label));
this.document.noteCitationRef(refnode);
}
else {
refnode = new nodes.footnote_reference(`[${label}]_`);
if (refname[0] === '#') {
refname = refname.substring(1);
refnode.attributes.auto = 1;
this.document.noteAutofootnoteRef(refnode);
}
else if (refname === '*') {
refname = '';
refnode.attributes.auto = '*';
this.document.noteSymbolFootnoteRef(refnode);
}
else {
refnode.add(new nodes.Text(label));
}
if (refname) {
refnode.attributes.refname = refname;
this.document.noteFootnoteRef(refnode);
}
if (getTrimFootnoteRefSpace(this.document.settings)) {
before = before.trimRight();
}
}
return [before, [refnode], remaining, []];
}
reference(match, lineno, anonymous = false) {
const referencename = match.groups.refname;
const refname = fullyNormalizeName(referencename);
const referencenode = new nodes.reference(referencename + match.groups.refend, referencename, [], { name: nodes.whitespaceNormalizeName(referencename) });
referencenode.getChild(0).rawsource = referencename;
if (anonymous) {
referencenode.attributes.anonymous = 1;
}
else {
referencenode.attributes.refname = refname;
this.document.noteRefname(referencenode);
}
const string = match.result.input;
const matchstart = match.result.index;
const matchend = match.result.index + match.result[0].length;
return [string.substring(0, matchstart), [referencenode], string.substring(matchend), []];
}
anonymous_reference(match, lineno) {
return this.reference(match, lineno, true);
}
problematic(text, rawsource, message) {
const msgid = this.document.setId(message, this.parent);
const problematic = new nodes.problematic(rawsource, text, [], { refid: msgid });
const prbid = this.document.setId(problematic);
message.addBackref(prbid);
return problematic;
}
emphasis(match, lineno) {
const [before, inlines, remaining, sysmessages, endstring] = this.inline_obj(match, lineno, this.patterns.emphasis, nodes.emphasis);
return [before, inlines, remaining, sysmessages];
}
strong(match, lineno) {
const [before, inlines, remaining, sysmessages, endstring] = this.inline_obj(match, lineno, this.patterns.strong, nodes.strong);
return [before, inlines, remaining, sysmessages];
}
interpreted_or_phrase_ref(match, lineno) {
const endPattern = this.patterns.interpreted_or_phrase_ref;
const string = match.match.input;
// console.log(match.groups);
const matchstart = match.match.index;
const matchend = matchstart + match.match[0].length;
const rolestart = matchstart;
let role = match.groups.role;
let position = '';
if (role) {
role = role.substring(1, role.length - 1);
position = 'prefix';
}
else if (this.quoted_start(match)) {
return [string.substring(0, matchend), [], string.substring(matchend), []];
}
const endmatch = endPattern.exec(string.substring(matchend));
if (endmatch && endmatch[1].length) {
const textend = matchend + endmatch.index + endmatch[0].length;
if (endmatch[3]) {
if (role) {
const msg = this.reporter.warning('Multiple roles in interpreted text (both '
+ 'prefix and suffix present; only one allowed).', [], { line: lineno });
const text = unescape(string.substring(rolestart, textend), true);
const prb = this.problematic(text, text, msg);
return [string.substring(0, rolestart), [prb], string.substring(textend), [msg]];
}
role = endmatch[3];
role = role.substring(1, role.length - 1);
position = 'suffix';
}
const escaped = endmatch.input.substring(0, endmatch.index);
let rawsource = unescape(string.substring(matchstart, textend), true);
if (rawsource[rawsource.length - 1] === '_') {
if (role) {
const msg = this.reporter.warning(`Mismatch: both interpreted text role ${position} and `
+ 'reference suffix.', [], { line: lineno });
const text = unescape(string.substring(rolestart, textend), true);
const prb = this.problematic(text, text, msg);
return [string.substring(0, rolestart), [prb], string.substring(textend), [msg]];
}
return this.phrase_ref(string.substring(0, matchstart), string.substring(textend), rawsource, escaped, unescape(escaped));
}
rawsource = unescape(string.substring(rolestart, textend), true);
const [nodelist, messages] = this.interpreted(rawsource, escaped, role, lineno);
return [string.substring(0, rolestart), nodelist, string.substring(textend), messages];
}
const msg = this.reporter.warning('Inline interpreted text or phrase reference start-string '
+ 'without end-string.', [], { line: lineno });
const text = unescape(string.substring(matchstart, matchend), true);
const prb = this.problematic(text, text, msg);
return [string.substring(0, match.match.index), [prb], string.substring(matchend), [msg]];
}
phrase_ref(before, after, rawsource, escaped, text) {
const match = this.patterns.embedded_link.exec(escaped);
let aliastype;
let aliastext;
let rawaliastext;
let rawtext;
let alias;
let target;
let aliasParts;
if (!rawsource) {
rawsource = '';
}
if (match) { // # embedded <URI> or <alias_>
text = unescape(escaped.substring(0, match.index));
rawtext = unescape(escaped.substring(0, match.index), true);
aliastext = unescape(match[2]);
rawaliastext = unescape(match[2], true);
const underscoreEscaped = rawaliastext.endsWith('\\_');
if (aliastext.endsWith('_') && !(underscoreEscaped
|| this.patterns.uri.exec(aliastext))) {
aliastype = 'name';
alias = fullyNormalizeName(aliastext.substring(0, aliastext.length - 1));
target = new nodes.target(match[1], '', [], { refname: alias });
target.indirectReferenceName = aliastext.substring(0, aliastext.length - 1);
}
else {
aliastype = 'uri';
aliasParts = splitEscapedWhitespace(match[2]);
/* this behaves differently from python's split with no args */
alias = aliasParts.map(part => unescape(part).split(/\s+/).join('')).join(' ');
// console.log(`alias is ${alias}`);
alias = this.adjustUri(alias);
if (alias.endsWith('\\_')) {
alias = `${alias.substring(0, alias.length - 2)}_`;
}
target = new nodes.target(match[1], '', [], { refuri: alias });
target.referenced = true;
}
if (!aliastext) {
throw new ApplicationError(`problem with embedded link: ${aliastext}`);
}
if (!text) {
text = alias;
rawtext = rawaliastext;
}
}
else {
target = null;
rawtext = unescape(escaped, true);
}
const refname = fullyNormalizeName(text);
const reference = new nodes.reference(rawsource, text, [], { name: nodes.whitespaceNormalizeName(text) });
reference.getChild(0).rawsource = rawtext;
const nodeList = [reference];
if (rawsource.endsWith('__')) {
if (target && (aliastype === 'name')) {
reference.attributes.refname = alias;
this.document.noteRefname(reference);
// commented out in docutils.
// # self.document.note_indirect_target(target) # required?
}
else if (target && (aliastype === 'uri')) {
reference.attributes.refuri = alias;
}
else {
reference.attributes.anonymous = 1;
}
}
else if (target) {
target.attributes.names.push(refname);
if (aliastype === 'name') {
reference.attributes.refname = alias;
this.document.noteIndirectTarget(target);
this.document.noteRefname(reference);
}
else {
reference.attributes.refuri = alias;
this.document.noteExplicitTarget(target, this.parent);
// original source commented out
// # target.note_referenced_by(name=refname)
}
nodeList.push(target);
}
else {
reference.attributes.refname = refname;
this.document.noteRefname(reference);
}
return [before, nodeList, after, []];
}
/** Test if inline markup start-string is 'quoted'.
'Quoted' in this context means the start-string is enclosed in a pair
of matching opening/closing delimiters (not necessarily quotes)
or at the end of the match.
*/
quoted_start(match) {
const string = match.result.input;
const start = match.result.index;
if (start === 0) { // start-string at beginning of text
return false;
}
let poststart;
const prestart = string[start - 1];
try {
poststart = string.substr(match.result.index + match.result[0].length, 1);
}
catch (error) {
// console.log(error.constructor.name);
return true; // not "quoted" but no markup start-string either
}
return matchChars(prestart, poststart);
}
inline_obj(match, lineno, endPattern, nodeclass, restore_backslashes = false) {
if (typeof nodeclass !== 'function') {
throw new Error();
}
if (!(endPattern instanceof RegExp)) {
throw new Error('');
}
// console.log(match);
const string = match.match.input;
const matchstart = string.indexOf(match.groups.start);
if (matchstart === -1) {
throw new Error('');
}
const matchend = matchstart + match.groups.start.length;
// console.log(`${matchstart} ${matchend}`);
if (this.quoted_start(match)) {
return [string.substring(0, matchend), [], string.substring(matchend), [], ''];
}
// console.log(endPattern);
const endmatch = endPattern.exec(string.substring(matchend));
let text;
let rawsource;
if (endmatch && endmatch.index) { // 1 or more chars
const _text = endmatch.input.substring(0, endmatch.index);
text = unescape(_text, restore_backslashes);
// this may not work for all situations
const textend = matchend + endmatch.index + endmatch[0].length;
rawsource = unescape(string.substring(matchstart, textend), true);
const node = new nodeclass(rawsource, text);
node.getChild(0).rawsource = unescape(_text, true);
return [string.substr(0, matchstart), [node], string.substr(textend), [], endmatch[1]];
}
const msg = this.reporter.warning(`Inline ${nodeclass.constructor.name} start-string without end-string.`, [], { line: lineno });
text = string.substring(matchstart, matchend); // unescape(string[matchstart:matchend], True)
rawsource = text; // unescape(string[matchstart:matchend], True)
const prb = this.problematic(text, rawsource, msg);
return [string.substring(0, matchstart), [prb], string.substring(matchend), [msg], ''];
}
initCustomizations(settings) {
let startStringPrefix;
let endStringSuffix;
let ssn;
let esn;
if (settings.characterLevelInlineMarkup) {
startStringPrefix = "(^|(?<!\\x00))";
ssn = [null, null];
endStringSuffix = "";
esn = [];
}
else {
startStringPrefix = ""; /* '(^|(?<=\\s|[' +
punctuation_chars.openers +
punctuation_chars.delimiters +
']))' */
ssn = [];
endStringSuffix = ""; /* '($|(?=\\s|[\\x00' + [
punctuation_chars.closing_delimiters,
punctuation_chars.delimiters,
punctuation_chars.closers].join('') +
']))' */
esn = [];
}
// this.simplename = '(?:(?!_)\\w)+(?:[-._+:](?:(?!_)\\w)+)*' // fixme
this.simplename = "\\w+";
// const prefix = startStringPrefix;
// const suffix = endStringSuffix;
/* const initialInline =
{ prefix, suffix: '',
parts: {
start: {
prefix: '',
suffix: this.nonWhitespaceAfter,
parts: ['\\*\\*','\\*(?!\\*)','``','_`', '\\|(?!\\|)'],
},
whole: {
prefix: '',
suffix,
parts:
*/
const parts = [
1, "initial_inline", startStringPrefix, "",
[
0,
[
1, "start", "", this.nonWhitespaceAfter, // simple start-strings
[
0, "\\*\\*", // strong
"\\*(?!\\*)", // emphasis but not strong
"``", // literal
"_`", // inline internal target
[2, "\\|(?!\\|)", null]
] // substitution reference
],
[1, "whole", "", endStringSuffix, // whole constructs
[0, // reference name & end-string
[2, `(${this.simplename})(__?)`, "refname", "refend"],
[1, "footnotelabel", "\\[", [2, "(\\]_)", "fnend"],
[0, "[0-9]+", // manually numbered
[2, `\\#(${this.simplename})?`, null], // auto-numbered (w/ label?)
"\\*", // auto-symbol
[2, `(${this.simplename})`, "citationlabel"]] // citation reference
]
]
],
[1, "backquote", // interpreted text or phrase reference
[2, `((:${this.simplename}:)?)`, "role", null], // optional role
this.nonWhitespaceAfter,
[0, [2, "`(?!`)", null]] // but not literal
]
]
];
this.startStringPrefix = startStringPrefix;
this.endStringSuffix = endStringSuffix;
this.parts = parts;
// const build = buildRegexp(parts, true);
// console.log(build[0]);
this.patterns = {
initial: buildRegexp(parts), // KM
emphasis: new RegExp(`${this.nonWhitespaceEscapeBefore}(\\*)${endStringSuffix}`),
strong: new RegExp(`${this.nonWhitespaceEscapeBefore}(\\*\\*)${endStringSuffix}`),
interpreted_or_phrase_ref: new RegExp(`${this.nonUnescapedWhitespaceEscapeBefore}(\`((:${this.simplename}:)?(__?)?))${endStringSuffix}`),
embedded_link: new RegExp(`((?:[ \\n]+|^)<${this.nonWhitespaceAfter}(([^<>]|\\x00[<>])+)${this.nonWhitespaceEscapeBefore}>)$`),
literal: new RegExp(`${this.nonWhitespaceBefore}(\`\`)${endStringSuffix}`),
target: new RegExp(`${this.nonWhitespaceEscapeBefore}(\`)${endStringSuffix}`),
substitution_ref: new RegExp(`${this.nonWhitespaceEscapeBefore}(\\|_{0,2})${endStringSuffix}`),
email: new RegExp(emailPattern), // fixme % args + '$',
// re.VERBOSE | re.UNICODE),
uri: new RegExp(`${startStringPrefix}((([a-zA-Z][a-zA-Z0-9.+-]*):(((//?)?${uric}*${uriEnd})(\\?${uric}*${uriEnd})?(\\#${uriEnd})?))|(${emailPattern}))${endStringSuffix}`),
// need pep
// need rfc
};
this.implicitDispatch.push(this.patterns.uri); //, this.standaloneUri);
/* if settings.pep_references:
self.implicit_dispatch.append((self.patterns.pep,
self.pep_reference))
if settings.rfc_references:
self.implicit_dispatch.append((self.patterns.rfc,
self.rfc_reference))
*/
}
parse(text, args) {
this.logger.silly('parse');
const { lineno, memo, parent } = args;
this.reporter = memo.reporter;
this.document = memo.document;
this.language = memo.language;
this.parent = parent;
let remaining = escape2null(text);
this.logger.silly('remaining', { value: remaining });
const processed = [];
let unprocessed = [];
const messages = [];
while (remaining) {
this.logger.debug(`checking pattern ${this.patterns.initial[0]}`);
const match = this.patterns.initial[0].exec(remaining);
// console.log(match);
if (match) {
this.logger.silly('matched', { value: match });
const rr = {};
this.patterns.initial[1].forEach((x, index) => {
if (x != null) {
rr[x] = match[index];
}
});
const mname = rr.start || rr.backquote || rr.refend || rr.fnend;
const method = this.dispatch[mname];
if (typeof method !== "function") {
throw new Error(`Invalid dispatch ${mname}`);
}
let before;
let inlines;
let sysmessages;
// console.log(`name is ${mname}`);
[before, inlines, remaining, sysmessages] = method({ result: match, match, groups: rr }, lineno);
unprocessed.push(before);
if (!isIterable(sysmessages)) {
throw new Error(`Expecting iterable, got ${sysmessages}`);
}
messages.push(...sysmessages);
if (inlines) {
processed.push(...this.implicit_inline(unprocessed.join(""), lineno));
processed.push(...inlines);
unprocessed = [];
}
}
else {
this.logger.silly('break');
break;
}
}
if (remaining) {
this.logger.silly('have remaining', { value: remaining });
processed.push(...this.implicit_inline(remaining, lineno));
}
// console.log(processed);
return [processed, messages];
}
/*
* Check each of the patterns in `self.implicit_dispatch` for a match,
* and dispatch to the stored method for the pattern. Recursively check
* the text before and after the match. Return a list of `nodes.Text`
* and inline element nodes.
*/
implicit_inline(text, lineno) {
this.logger.silly('implicit_inline', { value: text });
if (!text) {
return [];
}
/* for pattern, method in self.implicit_dispatch:
match = pattern.search(text)
if match:
try:
# Must recurse on strings before *and* after the match;
# there may be multiple patterns.
return (self.implicit_inline(text[:match.start()], lineno)
+ method(match, lineno) +
self.implicit_inline(text[match.end():], lineno))
except MarkupMismatch:
pass
*/
return [new nodes.Text(unescape(text), unescape(text, true))];
}
adjustUri(uri) {
return uri; // fixme
}
/*
//console.log(uri);
const match = this.patterns.email.exec(uri)
if(match) {
return 'mailto:' + uri
} else {
return uri
}
} */
interpreted(rawsource, text, role, lineno) {
const [roleFn, messages] = roleInterface(role, this.language, lineno, this.reporter);
if (roleFn) {
const [theNodes, messages2] = roleFn.invoke(role, rawsource, text, lineno, this);
try {
theNodes[0].getChild(0).rawsource = unescape(text, true);
}
catch (error) {
if (!(error instanceof TypeError)) {
throw error;
}
}
return [theNodes, [...messages, ...messages2]];
}
const msg = this.reporter.error(`Unknown interpreted text role "${role}".`, [], { line: lineno });
return [[this.problematic(rawsource, rawsource, msg)], [...messages, msg]];
}
literal(match, lineno) {
const [before, inlines, remaining, sysmessages, endstring] = this.inline_obj(match, lineno, this.patterns.literal, nodes.literal, true);
return [before, inlines, remaining, sysmessages];
}
}
export default Inliner;