@pacem/pacem-foundation 1.1.0-sierpinski → 2.0.0-babbage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @pacem/pacem-foundation v1.1.0-sierpinski (https://js.pacem.it)
2
+ * @pacem/pacem-foundation v2.0.0-babbage (https://js.pacem.it)
3
3
  * Pacem (https://pacem.it)
4
4
  * Licensed under Apache-2.0
5
5
  */
@@ -1425,11 +1425,8 @@
1425
1425
  if (width <= 0 || height <= 0 || isNaN(width) || isNaN(height)) {
1426
1426
  throw new Error('Invalid target size');
1427
1427
  }
1428
- if (typeof img === 'string') {
1429
- img = await Imaging.loadImage(img);
1430
- }
1431
- const el = await self.createImageBitmap(img);
1432
- const origWidth = el.width, origHeight = el.height;
1428
+ const el = await Imaging._toDrawable(img);
1429
+ const origWidth = el.width || width, origHeight = el.height || height;
1433
1430
  const cnv = new OffscreenCanvas(origWidth, origHeight);
1434
1431
  const ctx = cnv.getContext('2d');
1435
1432
  ctx.imageSmoothingEnabled =
@@ -1451,6 +1448,16 @@
1451
1448
  const retval = await cnv.convertToBlob({ quality, type });
1452
1449
  return retval;
1453
1450
  }
1451
+ /** Turns a URL or a `Blob` into a drawable source, while passing an already drawable source through untouched. */
1452
+ static async _toDrawable(img) {
1453
+ if (typeof img === 'string') {
1454
+ img = await Imaging.loadImage(img);
1455
+ }
1456
+ if (!(img instanceof Blob)) {
1457
+ return img;
1458
+ }
1459
+ return await self.createImageBitmap(img);
1460
+ }
1454
1461
  }
1455
1462
  // }
1456
1463
 
@@ -1778,6 +1785,600 @@
1778
1785
  };
1779
1786
  //}
1780
1787
 
1788
+ /** Token types shared by the built-in vocabularies (vocabularies may add their own). */
1789
+ var SyntaxTokenType;
1790
+ (function (SyntaxTokenType) {
1791
+ SyntaxTokenType["Text"] = "text";
1792
+ SyntaxTokenType["Space"] = "space";
1793
+ SyntaxTokenType["Newline"] = "newline";
1794
+ SyntaxTokenType["Comment"] = "comment";
1795
+ SyntaxTokenType["DocComment"] = "doc-comment";
1796
+ SyntaxTokenType["Keyword"] = "keyword";
1797
+ SyntaxTokenType["Identifier"] = "identifier";
1798
+ SyntaxTokenType["String"] = "string";
1799
+ SyntaxTokenType["Regex"] = "regex";
1800
+ SyntaxTokenType["Number"] = "number";
1801
+ SyntaxTokenType["Bracket"] = "bracket";
1802
+ SyntaxTokenType["Punctuation"] = "punctuation";
1803
+ SyntaxTokenType["Operator"] = "operator";
1804
+ SyntaxTokenType["Preprocessor"] = "preprocessor";
1805
+ SyntaxTokenType["Tag"] = "tag";
1806
+ SyntaxTokenType["TagName"] = "tagname";
1807
+ SyntaxTokenType["Attribute"] = "attribute";
1808
+ SyntaxTokenType["Entity"] = "entity";
1809
+ })(SyntaxTokenType || (SyntaxTokenType = {}));
1810
+
1811
+ //namespace Pacem.Compile {
1812
+ const registry = new Map();
1813
+ function normalize(name) {
1814
+ return (name ?? '').trim().toLowerCase();
1815
+ }
1816
+ /**
1817
+ * Registry of the {@link Syntax} vocabularies known to the {@link Scanner}, by (case-insensitive) name and aliases.
1818
+ * The built-in vocabularies (see {@link SyntaxName}) are registered upfront; more can be
1819
+ * registered at runtime, e.g. after fetching them as JSON.
1820
+ */
1821
+ class Syntaxes {
1822
+ /**
1823
+ * Registers (or replaces) a syntax under its name and aliases.
1824
+ * @param syntax The vocabulary to register.
1825
+ */
1826
+ static register(syntax) {
1827
+ if (syntax == null) {
1828
+ return;
1829
+ }
1830
+ const names = [syntax.name].concat(syntax.aliases ?? []);
1831
+ for (let name of names) {
1832
+ const key = normalize(name);
1833
+ registry.set(key, syntax);
1834
+ }
1835
+ }
1836
+ /**
1837
+ * Gets a registered syntax by name or alias.
1838
+ * @param nameOrAlias Case-insensitive name or alias (e.g. `'ts'`).
1839
+ * @returns The syntax, or `null` when unknown.
1840
+ */
1841
+ static get(nameOrAlias) {
1842
+ const key = normalize(nameOrAlias);
1843
+ return registry.get(key) ?? null;
1844
+ }
1845
+ /** Names (and aliases) of every registered syntax. */
1846
+ static names() {
1847
+ const keys = registry.keys();
1848
+ return Array.from(keys);
1849
+ }
1850
+ }
1851
+
1852
+ //namespace Pacem.Compile {
1853
+ const DEFAULT_START = 'root';
1854
+ const DEFAULT_KEYWORD_SOURCES = [SyntaxTokenType.Identifier];
1855
+ const DEFAULT_INSIGNIFICANT = [SyntaxTokenType.Space, SyntaxTokenType.Newline, SyntaxTokenType.Comment, SyntaxTokenType.DocComment];
1856
+ /** Consecutive zero-length transitions tolerated before a rule gets skipped (guards against push/pop loops). */
1857
+ const MAX_EMPTY_TRANSITIONS = 64;
1858
+ /** Base-syntax chain length tolerated before giving up (guards against `extends` cycles). */
1859
+ const MAX_EXTENDS_DEPTH = 16;
1860
+ function isCombined(item) {
1861
+ return item.combined != null;
1862
+ }
1863
+ const compiledCache = new WeakMap();
1864
+ function isIncludeRule(rule) {
1865
+ return 'include' in rule;
1866
+ }
1867
+ function toSet(values) {
1868
+ if (values == null || values.length === 0) {
1869
+ return null;
1870
+ }
1871
+ return new Set(values);
1872
+ }
1873
+ function uniqueFlags(flags) {
1874
+ const chars = flags.split('');
1875
+ const unique = new Set(chars);
1876
+ const array = Array.from(unique);
1877
+ return array.join('');
1878
+ }
1879
+ function mergeStates(own, base) {
1880
+ const merged = {};
1881
+ for (let name in base) {
1882
+ merged[name] = base[name].slice();
1883
+ }
1884
+ for (let name in own) {
1885
+ const baseRules = merged[name] ?? [];
1886
+ merged[name] = own[name].concat(baseRules);
1887
+ }
1888
+ return merged;
1889
+ }
1890
+ function mergeKeywords(own, base) {
1891
+ const merged = {};
1892
+ for (let type in base) {
1893
+ merged[type] = base[type].slice();
1894
+ }
1895
+ for (let type in own) {
1896
+ const baseWords = merged[type] ?? [];
1897
+ merged[type] = baseWords.concat(own[type]);
1898
+ }
1899
+ return merged;
1900
+ }
1901
+ /** Resolves the `extends` chain of `syntax` (own rules first, then the base ones). */
1902
+ function mergeSyntax(syntax, depth = 0) {
1903
+ const lexical = syntax.lexical ?? { states: {} };
1904
+ const own = {
1905
+ start: lexical.start,
1906
+ states: lexical.states ?? {},
1907
+ keywords: lexical.keywords ?? {},
1908
+ keywordSources: lexical.keywordSources,
1909
+ keywordExceptAfter: lexical.keywordExceptAfter,
1910
+ insignificant: lexical.insignificant
1911
+ };
1912
+ const baseSyntax = syntax.extends ? Syntaxes.get(syntax.extends) : null;
1913
+ if (baseSyntax == null || baseSyntax === syntax || depth >= MAX_EXTENDS_DEPTH) {
1914
+ return own;
1915
+ }
1916
+ const base = mergeSyntax(baseSyntax, depth + 1);
1917
+ return {
1918
+ start: own.start ?? base.start,
1919
+ states: mergeStates(own.states, base.states),
1920
+ keywords: mergeKeywords(own.keywords, base.keywords),
1921
+ keywordSources: own.keywordSources ?? base.keywordSources,
1922
+ keywordExceptAfter: own.keywordExceptAfter ?? base.keywordExceptAfter,
1923
+ insignificant: own.insignificant ?? base.insignificant
1924
+ };
1925
+ }
1926
+ /** Flattens `include` rules of a state (recursively, ignoring cycles). */
1927
+ function expandRules(states, stateName, visiting) {
1928
+ const rules = states[stateName] ?? [];
1929
+ if (visiting.has(stateName)) {
1930
+ return [];
1931
+ }
1932
+ visiting.add(stateName);
1933
+ const expanded = [];
1934
+ for (let rule of rules) {
1935
+ if (!isIncludeRule(rule)) {
1936
+ expanded.push(rule);
1937
+ continue;
1938
+ }
1939
+ const included = expandRules(states, rule.include, visiting);
1940
+ Array.prototype.push.apply(expanded, included);
1941
+ }
1942
+ visiting.delete(stateName);
1943
+ return expanded;
1944
+ }
1945
+ function compileCondition(condition) {
1946
+ if (condition == null) {
1947
+ return null;
1948
+ }
1949
+ return {
1950
+ previousTypes: toSet(condition.previousTypes),
1951
+ previousNotTypes: toSet(condition.previousNotTypes),
1952
+ previousTexts: toSet(condition.previousTexts),
1953
+ previousNotTexts: toSet(condition.previousNotTexts)
1954
+ };
1955
+ }
1956
+ function compileRule(rule) {
1957
+ const groupTypes = Array.isArray(rule.type) ? rule.type : null;
1958
+ const type = Array.isArray(rule.type) ? rule.type[0] : rule.type;
1959
+ // sticky and indices flags are the engine's business
1960
+ const userFlags = (rule.flags ?? '').replace(/[yd]/g, '');
1961
+ const ownFlags = uniqueFlags(userFlags);
1962
+ // the `d` flag yields the capture group offsets, needed to split a match into per-group tokens
1963
+ const flags = ownFlags + 'y' + (groupTypes ? 'd' : '');
1964
+ const regex = new RegExp(rule.match, flags);
1965
+ const until = rule.until ? new RegExp(rule.until, 'y') : null;
1966
+ const pop = rule.pop === true ? 1 : (typeof rule.pop === 'number' ? rule.pop : 0);
1967
+ const when = compileCondition(rule.when);
1968
+ return {
1969
+ regex, type, groupTypes, when, pop, until,
1970
+ source: rule.match,
1971
+ flags: ownFlags,
1972
+ next: rule.next ?? null,
1973
+ push: rule.push ?? null,
1974
+ embed: rule.embed ?? null,
1975
+ hasTransition: pop > 0 || !!rule.next || !!rule.push || !!rule.embed
1976
+ };
1977
+ }
1978
+ function compileKeywords(keywords) {
1979
+ const map = new Map();
1980
+ for (let type in keywords) {
1981
+ for (let word of keywords[type]) {
1982
+ map.set(word, type);
1983
+ }
1984
+ }
1985
+ return map;
1986
+ }
1987
+ /** Backreferences and named groups would break once renumbered/duplicated inside an alternation. */
1988
+ const UNMERGEABLE_SOURCE = /\\[1-9]|\\k<|\(\?<(?![=!])/;
1989
+ function countGroups(source, flags) {
1990
+ const probe = new RegExp(source + '|', flags);
1991
+ const match = probe.exec('');
1992
+ return match.length - 1;
1993
+ }
1994
+ function canMatchEmpty(source, flags) {
1995
+ const probe = new RegExp('^(?:' + source + ')$', flags);
1996
+ return probe.test('');
1997
+ }
1998
+ /** Whether a rule can be merged into a {@link CombinedSegment}. */
1999
+ function isMergeable(rule) {
2000
+ if (rule.when != null || UNMERGEABLE_SOURCE.test(rule.source)) {
2001
+ return false;
2002
+ }
2003
+ return !canMatchEmpty(rule.source, rule.flags);
2004
+ }
2005
+ function combine(rules) {
2006
+ if (rules.length === 1) {
2007
+ return { rule: rules[0], combined: null };
2008
+ }
2009
+ const groupIndices = [];
2010
+ let groupIndex = 1;
2011
+ for (let rule of rules) {
2012
+ groupIndices.push(groupIndex);
2013
+ const ownGroups = countGroups(rule.source, rule.flags);
2014
+ groupIndex += 1 + ownGroups;
2015
+ }
2016
+ const alternatives = rules.map(r => '(' + r.source + ')');
2017
+ const source = alternatives.join('|');
2018
+ const combined = new RegExp(source, rules[0].flags + 'y');
2019
+ return { rule: null, combined, rules, groupIndices };
2020
+ }
2021
+ /** Groups consecutive mergeable rules (sharing the same flags) into combined segments, keeping their order. */
2022
+ function segment(rules) {
2023
+ const segments = [];
2024
+ let pending = [];
2025
+ const flush = () => {
2026
+ if (pending.length > 0) {
2027
+ const combinedSegment = combine(pending);
2028
+ segments.push(combinedSegment);
2029
+ }
2030
+ pending = [];
2031
+ };
2032
+ for (let rule of rules) {
2033
+ if (!isMergeable(rule)) {
2034
+ flush();
2035
+ segments.push({ rule, combined: null });
2036
+ continue;
2037
+ }
2038
+ if (pending.length > 0 && pending[0].flags !== rule.flags) {
2039
+ flush();
2040
+ }
2041
+ pending.push(rule);
2042
+ }
2043
+ flush();
2044
+ return segments;
2045
+ }
2046
+ function compileSyntax(syntax) {
2047
+ const cached = compiledCache.get(syntax);
2048
+ if (cached != null) {
2049
+ return cached;
2050
+ }
2051
+ const merged = mergeSyntax(syntax);
2052
+ const states = new Map();
2053
+ for (let name in merged.states) {
2054
+ const visiting = new Set();
2055
+ const rules = expandRules(merged.states, name, visiting);
2056
+ const compiledRules = rules.map(compileRule);
2057
+ const segments = segment(compiledRules);
2058
+ states.set(name, segments);
2059
+ }
2060
+ const compiled = {
2061
+ name: syntax.name,
2062
+ start: merged.start ?? DEFAULT_START,
2063
+ states,
2064
+ keywords: compileKeywords(merged.keywords),
2065
+ keywordSources: new Set(merged.keywordSources ?? DEFAULT_KEYWORD_SOURCES),
2066
+ keywordExceptAfter: new Set(merged.keywordExceptAfter ?? []),
2067
+ insignificant: new Set(merged.insignificant ?? DEFAULT_INSIGNIFICANT)
2068
+ };
2069
+ compiledCache.set(syntax, compiled);
2070
+ return compiled;
2071
+ }
2072
+ function compileByName(name) {
2073
+ const syntax = Syntaxes.get(name);
2074
+ if (syntax == null) {
2075
+ return null;
2076
+ }
2077
+ return compileSyntax(syntax);
2078
+ }
2079
+ function meets(condition, previous) {
2080
+ const type = previous?.type ?? null, text = previous?.raw ?? null;
2081
+ if (condition.previousTypes != null && !condition.previousTypes.has(type)) {
2082
+ return false;
2083
+ }
2084
+ if (condition.previousNotTypes != null && condition.previousNotTypes.has(type)) {
2085
+ return false;
2086
+ }
2087
+ if (condition.previousTexts != null && !condition.previousTexts.has(text)) {
2088
+ return false;
2089
+ }
2090
+ return condition.previousNotTexts == null || !condition.previousNotTexts.has(text);
2091
+ }
2092
+ /** A single tokenization pass: holds the cursor, the state stack and the produced tokens. */
2093
+ class ScanRun {
2094
+ constructor(_source, root) {
2095
+ this._source = _source;
2096
+ this._tokens = [];
2097
+ /** Current scanning position. */
2098
+ this._position = 0;
2099
+ /** End of the already emitted tokens (characters between it and `_position` are pending unrecognized text). */
2100
+ this._emitted = 0;
2101
+ this._line = 1;
2102
+ this._column = 1;
2103
+ this._previous = null;
2104
+ this._emptyTransitions = 0;
2105
+ this._stack = [{ syntax: root, state: root.start, until: null }];
2106
+ }
2107
+ run() {
2108
+ const length = this._source.length;
2109
+ while (this._position < length) {
2110
+ this._step();
2111
+ }
2112
+ this._flushText();
2113
+ return this._tokens;
2114
+ }
2115
+ get _top() {
2116
+ const stack = this._stack;
2117
+ return stack[stack.length - 1];
2118
+ }
2119
+ _step() {
2120
+ const frame = this._top;
2121
+ if (this._popOnUntil(frame)) {
2122
+ return;
2123
+ }
2124
+ const found = this._findRule(frame);
2125
+ if (found == null) {
2126
+ // unrecognized character: accumulates into a `text` token
2127
+ this._position++;
2128
+ return;
2129
+ }
2130
+ this._flushText();
2131
+ this._emitMatch(found, frame.syntax);
2132
+ this._transition(found.rule, frame.syntax);
2133
+ const length = found.match[0].length;
2134
+ this._position += length;
2135
+ if (length > 0) {
2136
+ this._emptyTransitions = 0;
2137
+ }
2138
+ }
2139
+ /** Pops an embedded syntax as soon as its `until` pattern matches (without consuming input). */
2140
+ _popOnUntil(frame) {
2141
+ const until = frame.until;
2142
+ if (until == null || this._stack.length <= 1) {
2143
+ return false;
2144
+ }
2145
+ until.lastIndex = this._position;
2146
+ if (!until.test(this._source)) {
2147
+ return false;
2148
+ }
2149
+ this._flushText();
2150
+ this._stack.pop();
2151
+ return true;
2152
+ }
2153
+ _findRule(frame) {
2154
+ const segments = frame.syntax.states.get(frame.state) ?? [];
2155
+ for (let item of segments) {
2156
+ const found = isCombined(item) ? this._tryCombined(item) : this._trySingle(item.rule);
2157
+ if (found != null) {
2158
+ return found;
2159
+ }
2160
+ }
2161
+ return null;
2162
+ }
2163
+ _trySingle(rule) {
2164
+ const match = this._tryRule(rule);
2165
+ if (match == null) {
2166
+ return null;
2167
+ }
2168
+ return { rule, match };
2169
+ }
2170
+ /** Finds which merged rule matched (the first defined wrapping group), then re-runs it alone for its own groups. */
2171
+ _tryCombined(item) {
2172
+ const combined = item.combined;
2173
+ combined.lastIndex = this._position;
2174
+ const match = combined.exec(this._source);
2175
+ if (match == null) {
2176
+ return null;
2177
+ }
2178
+ const indices = item.groupIndices;
2179
+ for (let k = 0; k < indices.length; k++) {
2180
+ if (match[indices[k]] === undefined) {
2181
+ continue;
2182
+ }
2183
+ const rule = item.rules[k];
2184
+ return this._trySingle(rule);
2185
+ }
2186
+ return null;
2187
+ }
2188
+ _tryRule(rule) {
2189
+ if (rule.when != null && !meets(rule.when, this._previous)) {
2190
+ return null;
2191
+ }
2192
+ const regex = rule.regex;
2193
+ regex.lastIndex = this._position;
2194
+ const match = regex.exec(this._source);
2195
+ if (match == null) {
2196
+ return null;
2197
+ }
2198
+ if (match[0].length > 0) {
2199
+ return match;
2200
+ }
2201
+ // zero-length matches are only meaningful as (bounded) state transitions
2202
+ if (!rule.hasTransition || this._emptyTransitions >= MAX_EMPTY_TRANSITIONS) {
2203
+ return null;
2204
+ }
2205
+ this._emptyTransitions++;
2206
+ return match;
2207
+ }
2208
+ _emitMatch({ rule, match }, syntax) {
2209
+ const raw = match[0];
2210
+ if (rule.groupTypes == null) {
2211
+ this._append(rule.type, raw, syntax);
2212
+ return;
2213
+ }
2214
+ this._emitGroups(rule, match, syntax);
2215
+ }
2216
+ /** Splits a match into one token per (top-level) capture group; uncovered characters become `text` tokens. */
2217
+ _emitGroups(rule, match, syntax) {
2218
+ const indices = match.indices;
2219
+ const start = match.index, end = start + match[0].length, types = rule.groupTypes;
2220
+ let cursor = start;
2221
+ for (let i = 1; i < match.length; i++) {
2222
+ const range = indices[i];
2223
+ if (range == null || range[0] < cursor || range[1] === range[0]) {
2224
+ continue;
2225
+ }
2226
+ if (range[0] > cursor) {
2227
+ const gap = this._source.slice(cursor, range[0]);
2228
+ this._append(SyntaxTokenType.Text, gap, syntax);
2229
+ }
2230
+ const groupText = this._source.slice(range[0], range[1]);
2231
+ const groupType = types[i - 1] ?? types[types.length - 1];
2232
+ this._append(groupType, groupText, syntax);
2233
+ cursor = range[1];
2234
+ }
2235
+ if (cursor < end) {
2236
+ const rest = this._source.slice(cursor, end);
2237
+ this._append(SyntaxTokenType.Text, rest, syntax);
2238
+ }
2239
+ }
2240
+ _transition(rule, syntax) {
2241
+ const stack = this._stack;
2242
+ for (let i = 0; i < rule.pop && stack.length > 1; i++) {
2243
+ stack.pop();
2244
+ }
2245
+ if (rule.next != null) {
2246
+ this._top.state = rule.next;
2247
+ }
2248
+ if (rule.push != null) {
2249
+ stack.push({ syntax, state: rule.push, until: null });
2250
+ }
2251
+ if (rule.embed == null) {
2252
+ return;
2253
+ }
2254
+ const embedded = compileByName(rule.embed);
2255
+ if (embedded == null) {
2256
+ return;
2257
+ }
2258
+ stack.push({ syntax: embedded, state: embedded.start, until: rule.until });
2259
+ }
2260
+ /** Emits the pending unrecognized characters (if any) as a single `text` token. */
2261
+ _flushText() {
2262
+ if (this._emitted >= this._position) {
2263
+ return;
2264
+ }
2265
+ const text = this._source.slice(this._emitted, this._position);
2266
+ const syntax = this._top.syntax;
2267
+ this._append(SyntaxTokenType.Text, text, syntax);
2268
+ }
2269
+ _resolveType(type, raw, syntax) {
2270
+ if (!syntax.keywordSources.has(type)) {
2271
+ return type;
2272
+ }
2273
+ const previousText = this._previous?.raw;
2274
+ if (syntax.keywordExceptAfter.has(previousText)) {
2275
+ return type;
2276
+ }
2277
+ return syntax.keywords.get(raw) ?? type;
2278
+ }
2279
+ _append(type, raw, syntax) {
2280
+ const resolvedType = this._resolveType(type, raw, syntax);
2281
+ const token = {
2282
+ type: resolvedType, raw, text: raw,
2283
+ index: this._emitted,
2284
+ line: this._line,
2285
+ column: this._column,
2286
+ depth: this._stack.length - 1,
2287
+ syntax: syntax.name
2288
+ };
2289
+ this._tokens.push(token);
2290
+ this._advance(raw);
2291
+ if (!syntax.insignificant.has(resolvedType)) {
2292
+ this._previous = token;
2293
+ }
2294
+ }
2295
+ /** Moves the emitted offset and the line/column counters past `raw`. */
2296
+ _advance(raw) {
2297
+ this._emitted += raw.length;
2298
+ const lastNewline = raw.lastIndexOf('\n');
2299
+ if (lastNewline < 0) {
2300
+ this._column += raw.length;
2301
+ return;
2302
+ }
2303
+ for (let i = 0; i <= lastNewline; i++) {
2304
+ if (raw.charCodeAt(i) === 10) {
2305
+ this._line++;
2306
+ }
2307
+ }
2308
+ this._column = raw.length - lastNewline;
2309
+ }
2310
+ }
2311
+ // #endregion
2312
+ /** Used for unknown syntaxes: no rules, the whole source becomes a single `text` token. */
2313
+ const PLAIN_TEXT_SYNTAX = { name: SyntaxTokenType.Text, lexical: { states: {} } };
2314
+ /**
2315
+ * Language-agnostic lexical engine: tokenizes a source text according to a {@link Syntax} vocabulary.
2316
+ * - lossless: concatenating the tokens' `raw` reproduces the source exactly;
2317
+ * - linear: a single cursor and sticky regular expressions (compiled once per syntax);
2318
+ * - error tolerant: unrecognized characters become `text` tokens, it never throws.
2319
+ */
2320
+ class Scanner {
2321
+ /**
2322
+ * Tokenizes `source`.
2323
+ * @param source Source text.
2324
+ * @param syntax A {@link Syntax} or the name/alias of a registered one (see {@link Syntaxes}); an unknown one yields a single `text` token.
2325
+ * @returns The lossless, positioned token stream.
2326
+ */
2327
+ tokenize(source, syntax) {
2328
+ const text = source ?? '';
2329
+ const resolved = typeof syntax === 'string' ? Syntaxes.get(syntax) : syntax;
2330
+ const compiled = compileSyntax(resolved ?? PLAIN_TEXT_SYNTAX);
2331
+ const run = new ScanRun(text, compiled);
2332
+ return run.run();
2333
+ }
2334
+ }
2335
+
2336
+ //namespace Pacem.Compile {
2337
+ /** Default token type → CSS class mapping, matching the `code-*` classes styled by the theme (see `ui/markdown.css`). */
2338
+ const DefaultHighlightClasses = Object.freeze({
2339
+ 'keyword': 'code-keyword',
2340
+ 'preprocessor': 'code-keyword',
2341
+ 'string': 'code-string',
2342
+ 'regex': 'code-string',
2343
+ 'number': 'code-number',
2344
+ 'comment': 'code-comment',
2345
+ 'doc-comment': 'code-comment',
2346
+ 'tag': 'code-tag',
2347
+ 'tagname': 'code-tagname',
2348
+ 'attribute': 'code-attribute',
2349
+ });
2350
+ const HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' };
2351
+ function htmlEncode$1(text) {
2352
+ return text.replace(/[&<>"]/g, c => HTML_ESCAPES[c]);
2353
+ }
2354
+ /**
2355
+ * Renders a token stream (typically produced by the {@link Scanner}) as HTML: every token's `raw` text is HTML-encoded and,
2356
+ * when its type has a mapped class, wrapped in a `<span class="…">`.
2357
+ */
2358
+ class HighlightRenderer {
2359
+ /**
2360
+ * @param _classes Token type → CSS class mapping (defaults to {@link DefaultHighlightClasses}).
2361
+ */
2362
+ constructor(_classes = DefaultHighlightClasses) {
2363
+ this._classes = _classes;
2364
+ }
2365
+ render(tree) {
2366
+ let html = '';
2367
+ for (let token of tree ?? []) {
2368
+ html += this._renderToken(token);
2369
+ }
2370
+ return html;
2371
+ }
2372
+ _renderToken(token) {
2373
+ const encoded = htmlEncode$1(token.raw ?? '');
2374
+ const cssClass = this._classes[token.type];
2375
+ if (cssClass == null) {
2376
+ return encoded;
2377
+ }
2378
+ return `<span class="${cssClass}">${encoded}</span>`;
2379
+ }
2380
+ }
2381
+
1781
2382
  //namespace Pacem.Compile.Markdown {
1782
2383
  // #region HELPERS
1783
2384
  function htmlEncode(input) {
@@ -1795,6 +2396,84 @@
1795
2396
  }
1796
2397
  return input;
1797
2398
  }
2399
+ /** Splits a link/image destination into its url and optional title: `url "title"`, `url 'title'` or `url (title)`. */
2400
+ function splitUrlAndTitle(destination) {
2401
+ if (NullChecker.isNullOrEmpty(destination)) {
2402
+ return { url: destination };
2403
+ }
2404
+ const trimmed = destination.trim();
2405
+ const arr = /^(\S*)\s+(?:"([^"]*)"|'([^']*)'|\(([^)]*)\))\s*$/.exec(trimmed);
2406
+ if (arr == null) {
2407
+ return { url: trimmed };
2408
+ }
2409
+ const title = arr[2] ?? arr[3] ?? arr[4];
2410
+ return { url: arr[1], title };
2411
+ }
2412
+ function attributeEncode(input) {
2413
+ return (input ?? '').replace(/&(?!(?:#\d+|#x[\da-fA-F]+|\w+);)/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
2414
+ }
2415
+ /** Splits a table row into its (trimmed) cells: outer pipes are optional, `\|` is a literal pipe. */
2416
+ function splitTableCells(row) {
2417
+ let line = row.trim();
2418
+ if (line.startsWith('|')) {
2419
+ line = line.slice(1);
2420
+ }
2421
+ if (line.endsWith('|') && !line.endsWith('\\|')) {
2422
+ line = line.slice(0, -1);
2423
+ }
2424
+ const cells = line.split(/(?<!\\)\|/);
2425
+ return cells.map(c => c.trim().replace(/\\\|/g, '|'));
2426
+ }
2427
+ function tableAlignment(delimiter) {
2428
+ const left = delimiter.startsWith(':'), right = delimiter.endsWith(':');
2429
+ if (left && right) {
2430
+ return 'center';
2431
+ }
2432
+ if (right) {
2433
+ return 'right';
2434
+ }
2435
+ return left ? 'left' : null;
2436
+ }
2437
+ function indentationOf(line) {
2438
+ const arr = /^ */.exec(line);
2439
+ return arr[0].length;
2440
+ }
2441
+ /**
2442
+ * Splits a list block into its items, by indentation: a marker indented less than the current item's content
2443
+ * starts a sibling item; anything else (nested lists included) is item content, dedented by the content offset.
2444
+ */
2445
+ function splitListItems(raw) {
2446
+ const lines = raw.replace(/\t/g, ' ').split(/\r?\n/);
2447
+ const items = [];
2448
+ for (let line of lines) {
2449
+ const current = items[items.length - 1];
2450
+ const marker = LISTMARKER.exec(line);
2451
+ if (marker != null && (current == null || marker[1].length < current.indent)) {
2452
+ const contentIndent = marker[0].length;
2453
+ const firstLine = line.slice(contentIndent);
2454
+ items.push({ indent: contentIndent, lines: [firstLine] });
2455
+ continue;
2456
+ }
2457
+ if (current == null) {
2458
+ continue;
2459
+ }
2460
+ const dedent = Math.min(current.indent, indentationOf(line));
2461
+ const content = line.slice(dedent);
2462
+ current.lines.push(content);
2463
+ }
2464
+ return items.map(i => i.lines.join('\n').replace(/\s+$/, ''));
2465
+ }
2466
+ /**
2467
+ * Code meant for a syntax highlighter: `MarkdownService` escapes every `<` of its input (no html allowed), hence
2468
+ * `&lt;` is turned back into `<` (the highlighter encodes everything it renders anyway).
2469
+ */
2470
+ function decodeCode(code) {
2471
+ return (code ?? '').replace(/&lt;/g, '<');
2472
+ }
2473
+ /** Highlights fenced code blocks of the registered languages (see {@link Syntaxes}). */
2474
+ const scanner = new Scanner();
2475
+ /** Fenced code in this vocabulary also gets the `code-markup` class. */
2476
+ const MARKUP_SYNTAX = 'markup';
1798
2477
  function regexMatchIndex(arr) {
1799
2478
  return arr?.index || void 0;
1800
2479
  }
@@ -1822,27 +2501,51 @@
1822
2501
  }
1823
2502
  // #endregion
1824
2503
  const COMMENT = /(<!--)([\s\S]*?)(-->|$)/;
1825
- const HR = /^ {0,3}(={3,}|_{3,}|-{3,})(\r?\n|$)/;
2504
+ const HR = /^ {0,3}(={3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,}|(?:\*[ \t]*){3,})(\r?\n|$)/;
1826
2505
  const HEADING = /^ {0,3}(#{1,6})\s+([^\n]+)(\r?\n|$)+/;
1827
2506
  const HEADING1 = /^ {0,3}([^\n]+)(\r?\n=+(\r?\n|$))+/;
1828
2507
  const HEADING2 = /^ {0,3}([^\n]+)(\r?\n-+(\r?\n|$))+/;
1829
2508
  const CODEBLOCK = /^ {4,}.+(\r?\n {4,}.+)*(\r?\n|$)+/;
1830
2509
  const FENCES = /^ {0,3}``` *([^\n]*)\r?\n(([\s\S](?!\r?\n {0,3}```))*.(\r?\n)+) {0,3}```(\r?\n|$)+/;
1831
2510
  const BLOCKQUOTE = /^ {0,3}> *([^\n]+)(\r?\n {0,3}> *([^\n]*))*(\r?\n|$)+/;
1832
- const ULIST = /^( {0,3}([-+]).*\r?\n|$)((?!(?: {0,3}(?:\d+[.\)]|\r?\n\S))).*(\r?\n|$))*/;
1833
- const OLIST = /^( {0,3}(\d+[.\)]).*\r?\n|$)((?!(?: {0,3}(?:[+-]|\r?\n\S))).*(\r?\n|$))*/;
1834
- const LISTITEM = /^(?:[-+*]|\d+[.\)])/m;
1835
- const PARAGRAPH = /^(?! {4,})(.+(\n(?! {0,3}```)(?! {0,3}([-+]|\d+[.\)]))(?! {4,})(?! {0,3}>).+)*)/;
2511
+ // (`*` bullets need a following space, not to be mistaken for emphasis; nested lists of the other kind are indented)
2512
+ const ULIST = /^( {0,3}([-+]|\*(?=[ \t])).*(\r?\n|$))((?!(?: ?\d+[.\)]|\r?\n\S)).*(\r?\n|$))*/;
2513
+ const OLIST = /^( {0,3}(\d+[.\)]).*(\r?\n|$))((?!(?: {0,2}(?:[-+]|\*[ \t])|\r?\n\S)).*(\r?\n|$))*/;
2514
+ /** A list item marker, with its indentation and the spaces following it. */
2515
+ const LISTMARKER = /^( *)([-+*]|\d+[.\)])([ \t]+|$)/;
2516
+ /** GFM task list item: `[ ]` or `[x]` at the beginning of the item. */
2517
+ const TASK = /^\[([ xX])\][ \t]+/;
2518
+ // (whitespace-only lines are blank lines: they never start nor continue a paragraph)
2519
+ const PARAGRAPH = /^(?! {4,})(?=[ \t]*\S)(.+(\n(?![ \t]*(?:\n|$))(?! {0,3}```)(?! {0,3}([-+]|\*[ \t]|\d+[.\)]))(?! {0,3}#{1,6}[ \t])(?! {4,})(?! {0,3}>).+)*)/;
1836
2520
  const REF = /^ {0,3}(!?)\[([^\]]+)\]: +(\S+)(?: +['"\(](.+)['"\)])?(\r?\n|$)+/;
1837
- const IMAGE_OR_LINK = /!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?(\[[^\[\]]*\]|\([^\(\)]*\))?/;
2521
+ // (an escaped `\[` never opens a link)
2522
+ const IMAGE_OR_LINK = /(?<!\\)!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?(\[[^\[\]]*\]|\([^\(\)]*\))?/;
1838
2523
  const EMPHASIZED_LINK = /(\*\*?|__?)(\[[^\]]*]\([^\)]+\))\1/;
1839
2524
  const CODE = /(?:(?<!\\)`)(.+?)(?:(?<!\\)`)/;
1840
- const EMPHASIZE = /(\*\*?|__?)((?!\1\s?)(?:(?!\1)[\S\s])*[^\s])\1/;
1841
- const STRIKE = /(~~?|__?)(?!\1\s?)([^\s][\s\S]*?(?!\1)[^\s])\1/;
2525
+ // (underscores never emphasize inside words: `snake_case_word`)
2526
+ const EMPHASIZE = /(\*\*?)((?!\1\s?)(?:(?!\1)[\S\s])*[^\s])\1|(?<![A-Za-z0-9])(__?)((?!\3\s?)(?:(?!\3)[\S\s])*[^\s])\3(?![A-Za-z0-9])/;
2527
+ // (underscores are emphasis markers only: `snake_case_word` must not strike anything through)
2528
+ const STRIKE = /(~~?)(?!\1\s?)([^\s][\s\S]*?(?!\1)[^\s])\1/;
1842
2529
  const QUOTE = /(:")(?!":\s?)([^\s][\s\S]*?(?!":)[^\s])":/;
1843
2530
  const SPACE = /^\s+/;
1844
- const LINEBREAK = / {2,}\r?\n/;
2531
+ /** Blank lines between blocks (it keeps the indentation of the next line, which matters to indented code blocks). */
2532
+ const BLOCK_SPACE = /^(?:[ \t]*\r?\n)+|^\s+/;
2533
+ const LINEBREAK = /(?: {2,}|\\)\r?\n/;
1845
2534
  const TEXT = /^[^\r\n]+/;
2535
+ /** Backslash escapes of ASCII punctuation (CommonMark); `\&lt;` is an escaped `<` already escaped by `MarkdownService`. */
2536
+ const ESCAPE = /\\(&lt;|[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~])/;
2537
+ /** Escaped characters that must stay text once rendered as html. */
2538
+ const ESCAPED_HTML = { '<': '&lt;', '&lt;': '&lt;', '&': '&amp;' };
2539
+ /** `<https://…>`, `<mailto:…>` and `<user@domain>` (the `<` may come escaped, e.g. by `MarkdownService`). */
2540
+ const AUTOLINK = /(?:<|&lt;)((?:https?|ftp|mailto):[^\s<>]+|[\w.+-]+@[\w-]+(?:\.[\w-]+)+)>/;
2541
+ /** GFM bare URLs (`https://…`, `www.…`), trailing punctuation excluded. */
2542
+ const BARE_URL = /(?<![\w\/\\(\[<"'=;&@])(?:https?:\/\/|www\.)[^\s<>]*[^\s<>.,:;"'!?)\]*_~]/;
2543
+ /** GFM footnote reference `[^id]`. */
2544
+ const FOOTNOTE_REF = /\[\^([^\]\s]+)\]/;
2545
+ /** GFM footnote definition `[^id]: text` (continuation lines indented). */
2546
+ const FOOTNOTE_DEF = /^ {0,3}\[\^([^\]\s]+)\]: *([^\r\n]*(?:\r?\n {2,}[^\r\n]+)*)(\r?\n|$)+/;
2547
+ /** GFM table: header row, delimiter row, then body rows (up to a blank line). */
2548
+ const TABLE = /^ {0,3}(\|?[^\r\n]*\|[^\r\n]*)\r?\n {0,3}(\|? *:?-+:? *(?:\| *:?-+:? *)*\|?) *(?:\r?\n|$)((?:(?! {0,3}(?:\r?\n|$))[^\r\n]*\|[^\r\n]*(?:\r?\n|$))*)/;
1846
2549
  var MARKDOWN_TYPES;
1847
2550
  (function (MARKDOWN_TYPES) {
1848
2551
  MARKDOWN_TYPES["Html"] = "html";
@@ -1867,6 +2570,11 @@
1867
2570
  MARKDOWN_TYPES["UnorderedList"] = "ul";
1868
2571
  MARKDOWN_TYPES["ListItem"] = "listitem";
1869
2572
  MARKDOWN_TYPES["Reference"] = "ref";
2573
+ MARKDOWN_TYPES["Table"] = "table";
2574
+ MARKDOWN_TYPES["TableRow"] = "tr";
2575
+ MARKDOWN_TYPES["TableCell"] = "td";
2576
+ MARKDOWN_TYPES["FootnoteReference"] = "fnref";
2577
+ MARKDOWN_TYPES["FootnoteDefinition"] = "fndef";
1870
2578
  })(MARKDOWN_TYPES || (MARKDOWN_TYPES = {}));
1871
2579
  var MARKDOWN_RULES;
1872
2580
  (function (MARKDOWN_RULES) {
@@ -1889,6 +2597,12 @@
1889
2597
  MARKDOWN_RULES[MARKDOWN_RULES["Quote"] = 16] = "Quote";
1890
2598
  MARKDOWN_RULES[MARKDOWN_RULES["Code"] = 17] = "Code";
1891
2599
  MARKDOWN_RULES[MARKDOWN_RULES["LineBreak"] = 18] = "LineBreak";
2600
+ MARKDOWN_RULES[MARKDOWN_RULES["FootnoteDefinition"] = 19] = "FootnoteDefinition";
2601
+ MARKDOWN_RULES[MARKDOWN_RULES["Table"] = 20] = "Table";
2602
+ MARKDOWN_RULES[MARKDOWN_RULES["Escape"] = 21] = "Escape";
2603
+ MARKDOWN_RULES[MARKDOWN_RULES["AutoLink"] = 22] = "AutoLink";
2604
+ MARKDOWN_RULES[MARKDOWN_RULES["FootnoteReference"] = 23] = "FootnoteReference";
2605
+ MARKDOWN_RULES[MARKDOWN_RULES["BareUrl"] = 24] = "BareUrl";
1892
2606
  // lowest priority
1893
2607
  MARKDOWN_RULES[MARKDOWN_RULES["Text"] = 254] = "Text";
1894
2608
  MARKDOWN_RULES[MARKDOWN_RULES["Space"] = 255] = "Space";
@@ -1909,7 +2623,8 @@
1909
2623
  type: MARKDOWN_TYPES.Html, index, raw: arr[0], text: arr[2]
1910
2624
  }];
1911
2625
  if (index > 0) {
1912
- const left = input.substr(0, index), tokens = lexer.tokenize(left, blockRules);
2626
+ // (the top-level rules: reference and footnote definitions may precede a comment)
2627
+ const left = input.substr(0, index), tokens = lexer.tokenize(left, topRules);
1913
2628
  Array.prototype.unshift.apply(output, tokens);
1914
2629
  }
1915
2630
  return output;
@@ -1924,6 +2639,16 @@
1924
2639
  }];
1925
2640
  })
1926
2641
  },
2642
+ // footnote definition (rendered at the end of the document, see HtmlRenderer)
2643
+ [MARKDOWN_RULES.FootnoteDefinition]: {
2644
+ exec: (input, lexer) => match(input, FOOTNOTE_DEF, arr => {
2645
+ const raw = arr[0], id = normalizeReference(arr[1]), text = arr[2].replace(/\r?\n {2,}/g, ' ');
2646
+ return [{
2647
+ type: MARKDOWN_TYPES.FootnoteDefinition, index: regexMatchIndex(arr), raw, text, id,
2648
+ tokens: lexer.tokenize(text, inlineRules)
2649
+ }];
2650
+ })
2651
+ },
1927
2652
  };
1928
2653
  const blockExtra = [], inlineExtra = [], codeExtra = {};
1929
2654
  for (let r of extra) {
@@ -1982,21 +2707,61 @@
1982
2707
  // link/image
1983
2708
  [MARKDOWN_RULES.ImageOrLink]: {
1984
2709
  exec: (input, lexer) => inlineMatch(input, IMAGE_OR_LINK, lexer, arr => {
1985
- const index = regexMatchIndex(arr), raw = arr[0], image = raw.charAt(0) === '!', text = arr[1], url = extractUrl(arr[2]), ref = normalizeReference(extractReference(arr[2]) ?? text);
2710
+ const index = regexMatchIndex(arr), raw = arr[0], image = raw.charAt(0) === '!', text = arr[1], destination = extractUrl(arr[2]), reference = extractReference(arr[2]) ?? text, ref = normalizeReference(reference);
2711
+ // `(url "title")`
2712
+ const { url, title } = splitUrlAndTitle(destination);
2713
+ const tokens = lexer.tokenize(text, inlineRules);
1986
2714
  if (image) {
1987
2715
  return [{
1988
2716
  index, type: url ? MARKDOWN_TYPES.Image : MARKDOWN_TYPES.ImageLike,
1989
- raw, text, src: url, ref,
1990
- tokens: lexer.tokenize(text, inlineRules)
1991
- }];
1992
- }
1993
- else {
1994
- return [{
1995
- index, type: url ? MARKDOWN_TYPES.Link : MARKDOWN_TYPES.LinkLike,
1996
- raw, text, href: url, ref,
1997
- tokens: lexer.tokenize(text, inlineRules)
2717
+ raw, text, src: url, title, ref, tokens
1998
2718
  }];
1999
2719
  }
2720
+ return [{
2721
+ index, type: url ? MARKDOWN_TYPES.Link : MARKDOWN_TYPES.LinkLike,
2722
+ raw, text, href: url, title, ref, tokens
2723
+ }];
2724
+ })
2725
+ },
2726
+ // backslash escapes: the escaped character is plain text
2727
+ [MARKDOWN_RULES.Escape]: {
2728
+ exec: (input, lexer) => inlineMatch(input, ESCAPE, lexer, arr => {
2729
+ const text = ESCAPED_HTML[arr[1]] ?? arr[1];
2730
+ return [{
2731
+ index: regexMatchIndex(arr), raw: arr[0], text, type: MARKDOWN_TYPES.Text
2732
+ }];
2733
+ })
2734
+ },
2735
+ // <https://…>, <user@domain>
2736
+ [MARKDOWN_RULES.AutoLink]: {
2737
+ exec: (input, lexer) => inlineMatch(input, AUTOLINK, lexer, arr => {
2738
+ const text = arr[1];
2739
+ const isEmail = !/^[a-z]+:/i.test(text);
2740
+ const href = isEmail ? 'mailto:' + text : text;
2741
+ return [{
2742
+ index: regexMatchIndex(arr), raw: arr[0], text, href, type: MARKDOWN_TYPES.Link,
2743
+ tokens: [{ type: MARKDOWN_TYPES.Text, raw: text, text }]
2744
+ }];
2745
+ })
2746
+ },
2747
+ // bare https://… or www.… urls
2748
+ [MARKDOWN_RULES.BareUrl]: {
2749
+ exec: (input, lexer) => inlineMatch(input, BARE_URL, lexer, arr => {
2750
+ const text = arr[0];
2751
+ const href = /^www\./i.test(text) ? 'http://' + text : text;
2752
+ return [{
2753
+ index: regexMatchIndex(arr), raw: text, text, href, type: MARKDOWN_TYPES.Link,
2754
+ tokens: [{ type: MARKDOWN_TYPES.Text, raw: text, text }]
2755
+ }];
2756
+ })
2757
+ },
2758
+ // [^footnote]
2759
+ [MARKDOWN_RULES.FootnoteReference]: {
2760
+ exec: (input, lexer) => inlineMatch(input, FOOTNOTE_REF, lexer, arr => {
2761
+ const id = normalizeReference(arr[1]);
2762
+ return [{
2763
+ index: regexMatchIndex(arr), raw: arr[0], text: arr[1], id, type: MARKDOWN_TYPES.FootnoteReference
2764
+ }];
2000
2765
  })
2001
2766
  },
2002
2767
  // code
@@ -2024,10 +2789,12 @@
2024
2789
  // italic/bold
2025
2790
  [MARKDOWN_RULES.Emphasize]: {
2026
2791
  exec: (input, lexer) => inlineMatch(input, EMPHASIZE, lexer, arr => {
2027
- if (arr.length < 3 || NullChecker.isNullOrEmpty(arr[2])) {
2792
+ // `*` (groups 1-2) or word-bounded `_` (groups 3-4) markers
2793
+ const marker = arr[1] ?? arr[3], text = arr[2] ?? arr[4];
2794
+ if (NullChecker.isNullOrEmpty(text)) {
2028
2795
  return null;
2029
2796
  }
2030
- const type = arr[1].length > 1 ? MARKDOWN_TYPES.Bold : MARKDOWN_TYPES.Italic, text = arr[2];
2797
+ const type = marker.length > 1 ? MARKDOWN_TYPES.Bold : MARKDOWN_TYPES.Italic;
2031
2798
  return [{
2032
2799
  index: regexMatchIndex(arr),
2033
2800
  type, raw: arr[0], text, tokens: lexer.tokenize(text, inlineRules)
@@ -2096,7 +2863,20 @@
2096
2863
  inPlaceInlineExec(input, output, lexer);
2097
2864
  return output;
2098
2865
  };
2099
- const inlineRules = Object.values(inline);
2866
+ // explicit priorities: footnote references before `[links]`, escapes before emphasis, bare urls last
2867
+ const inlineRules = [
2868
+ inline[MARKDOWN_RULES.FootnoteReference],
2869
+ inline[MARKDOWN_RULES.EmphasizedLink],
2870
+ inline[MARKDOWN_RULES.ImageOrLink],
2871
+ inline[MARKDOWN_RULES.Escape],
2872
+ inline[MARKDOWN_RULES.AutoLink],
2873
+ inline[MARKDOWN_RULES.Emphasize],
2874
+ inline[MARKDOWN_RULES.StrikeThrough],
2875
+ inline[MARKDOWN_RULES.Quote],
2876
+ inline[MARKDOWN_RULES.Code],
2877
+ inline[MARKDOWN_RULES.LineBreak],
2878
+ inline[MARKDOWN_RULES.BareUrl],
2879
+ ];
2100
2880
  Array.prototype.push.apply(inlineRules, inlineExtra);
2101
2881
  inlineRules.push(inlinelowpriority[MARKDOWN_RULES.Text], inlinelowpriority[MARKDOWN_RULES.Space]);
2102
2882
  // #endregion
@@ -2115,7 +2895,12 @@
2115
2895
  // paragraph
2116
2896
  [MARKDOWN_RULES.Paragraph]: {
2117
2897
  exec: (input, lexer) => match(input, PARAGRAPH, arr => {
2118
- const raw = arr[0], text = raw;
2898
+ // leading whitespace of paragraph lines is not content (CommonMark)
2899
+ const raw = arr[0], text = raw.replace(/^[ \t]+/gm, '');
2900
+ // whitespace-only lines are blank lines, not (empty) paragraphs
2901
+ if (text.trim().length === 0) {
2902
+ return null;
2903
+ }
2119
2904
  return [{
2120
2905
  type: MARKDOWN_TYPES.Paragraph, raw, text,
2121
2906
  // recursion (inline rules only)
@@ -2123,12 +2908,57 @@
2123
2908
  }];
2124
2909
  })
2125
2910
  },
2126
- // sparse lines
2127
- [MARKDOWN_RULES.Space]: inlinelowpriority[MARKDOWN_RULES.Space]
2911
+ // blank lines (the indentation of the following line is kept: it matters to indented code blocks)
2912
+ [MARKDOWN_RULES.Space]: {
2913
+ exec: (input) => match(input, BLOCK_SPACE, arr => {
2914
+ const raw = arr[0];
2915
+ return [{
2916
+ type: MARKDOWN_TYPES.Space, raw, text: raw
2917
+ }];
2918
+ })
2919
+ }
2128
2920
  };
2129
2921
  const tokenizeListItem = (text, lexer) => {
2130
- const grammar = !/^ {0,3}(`{3,}|$|>|[+-]\s|\d+[.\)])/m.test(text) ? inlineRules : blockRules;
2131
- return lexer.tokenize(text, grammar);
2922
+ const needsBlocks = /^ {0,3}(`{3,}|$|>|[-+*]\s|\d+[.\)]|#{1,6}\s|\|)/m.test(text);
2923
+ if (!needsBlocks) {
2924
+ return lexer.tokenize(text, inlineRules);
2925
+ }
2926
+ const tokens = lexer.tokenize(text, blockRules);
2927
+ // tight items (no blank lines) keep their first paragraph unwrapped, e.g. `<li>a<ul>…</ul></li>`
2928
+ const first = tokens[0];
2929
+ const tight = !/\n[ \t]*\n/.test(text);
2930
+ if (!tight || first?.type !== MARKDOWN_TYPES.Paragraph) {
2931
+ return tokens;
2932
+ }
2933
+ const inner = first.tokens ?? [];
2934
+ const rest = tokens.slice(1);
2935
+ return inner.concat(rest);
2936
+ };
2937
+ const tokenizeListItems = (raw, lexer) => {
2938
+ const items = splitListItems(raw);
2939
+ const nonEmpty = items.filter(i => !NullChecker.isNullOrEmpty(i));
2940
+ return nonEmpty.map(item => {
2941
+ // GFM task list item: `[ ]`/`[x]`
2942
+ const task = TASK.exec(item);
2943
+ const text = task ? item.slice(task[0].length) : item;
2944
+ const tokens = tokenizeListItem(text, lexer);
2945
+ if (task == null) {
2946
+ return { type: MARKDOWN_TYPES.ListItem, raw: item, text, tokens };
2947
+ }
2948
+ const checked = task[1] !== ' ';
2949
+ return { type: MARKDOWN_TYPES.ListItem, raw: item, text, tokens, task: true, checked };
2950
+ });
2951
+ };
2952
+ const tokenizeTableRow = (cells, align, header, lexer) => {
2953
+ const tokens = align.map((cellAlign, i) => {
2954
+ // missing cells are empty, exceeding ones are ignored (GFM)
2955
+ const text = cells[i] ?? '';
2956
+ const cellTokens = lexer.tokenize(text, inlineRules);
2957
+ return { type: MARKDOWN_TYPES.TableCell, raw: text, text, header, align: cellAlign, tokens: cellTokens };
2958
+ });
2959
+ const raw = cells.join(' | ');
2960
+ const row = { type: MARKDOWN_TYPES.TableRow, raw, text: raw, header, tokens };
2961
+ return row;
2132
2962
  };
2133
2963
  const block = {
2134
2964
  // hr
@@ -2143,7 +2973,8 @@
2143
2973
  // headings
2144
2974
  [MARKDOWN_RULES.Heading]: {
2145
2975
  exec: (input, lexer) => match(input, HEADING, arr => {
2146
- const text = arr[2];
2976
+ // optional closing sequence: `## Title ##`
2977
+ const text = arr[2].replace(/[ \t]+#+[ \t]*$/, '');
2147
2978
  return [{
2148
2979
  type: MARKDOWN_TYPES.Heading, raw: arr[0], text, level: arr[1].length, ref: normalizeReference(text),
2149
2980
  tokens: lexer.tokenize(text, inlineRules)
@@ -2171,60 +3002,86 @@
2171
3002
  // codeblock
2172
3003
  [MARKDOWN_RULES.CodeBlock]: {
2173
3004
  exec: (input, lexer) => match(input, CODEBLOCK, arr => {
2174
- const raw = arr[0], text = htmlEncode(raw);
3005
+ // indented code: 4 spaces of indentation are not part of the code
3006
+ const raw = arr[0], code = raw.replace(/^ {4}/gm, '').replace(/\s+$/, ''), text = htmlEncode(code);
3007
+ // whitespace-only indented lines are blank lines, not code
3008
+ if (code.length === 0) {
3009
+ return null;
3010
+ }
2175
3011
  return [{
2176
3012
  type: MARKDOWN_TYPES.CodeBlock, raw, text,
2177
- tokens: lexer.tokenize(text, [])
3013
+ tokens: lexer.tokenize(text, getLangGrammar(''))
2178
3014
  }];
2179
3015
  })
2180
3016
  },
2181
3017
  // codeblock
2182
3018
  [MARKDOWN_RULES.Fences]: {
2183
3019
  exec: (input, lexer) => match(input, FENCES, arr => {
2184
- const text = htmlEncode(arr[2]), lang = arr[1]?.toLowerCase();
3020
+ // the info string may carry more than the language (e.g. ```js title)
3021
+ const info = (arr[1] ?? '').trim(), words = info.split(/\s+/), lang = words[0].toLowerCase();
3022
+ // custom `code` rules (see ExtraRule) win over the registered vocabularies
3023
+ const syntax = lang in codeExtra ? null : Syntaxes.get(lang);
3024
+ if (syntax == null) {
3025
+ const text = htmlEncode(arr[2]);
3026
+ return [{
3027
+ type: MARKDOWN_TYPES.CodeBlock, raw: arr[0], text, lang,
3028
+ tokens: lexer.tokenize(text, getLangGrammar(lang))
3029
+ }];
3030
+ }
3031
+ // highlighted by the language-agnostic Scanner (see HtmlRenderer)
3032
+ const code = decodeCode(arr[2]);
3033
+ const tokens = scanner.tokenize(code, syntax);
2185
3034
  return [{
2186
- type: MARKDOWN_TYPES.CodeBlock, raw: arr[0], text, lang,
2187
- tokens: lexer.tokenize(text, getLangGrammar(lang))
3035
+ type: MARKDOWN_TYPES.CodeBlock, raw: arr[0], text: code, lang, syntax: syntax.name, highlighted: true, tokens
2188
3036
  }];
2189
3037
  })
2190
3038
  },
2191
3039
  // blockquote
2192
3040
  [MARKDOWN_RULES.BlockQuote]: {
2193
3041
  exec: (input, lexer) => match(input, BLOCKQUOTE, arr => {
2194
- const raw = arr[0], text = raw.replace(/^ {0,3}> */gm, '');
3042
+ const raw = arr[0], text = raw.replace(/^ {0,3}> ?/gm, '');
2195
3043
  return [{
2196
3044
  type: MARKDOWN_TYPES.BlockQuote, raw, text,
2197
- // recursion
2198
- tokens: lexer.tokenize(text, [
2199
- lowpriority[MARKDOWN_RULES.Paragraph],
2200
- lowpriority[MARKDOWN_RULES.Space]
2201
- ])
3045
+ // recursion: any block (nested quotes, lists, code…)
3046
+ tokens: lexer.tokenize(text, blockRules)
2202
3047
  }];
2203
3048
  })
2204
3049
  },
2205
3050
  // lists
2206
3051
  [MARKDOWN_RULES.OrderedList]: {
2207
3052
  exec: (input, lexer) => match(input, OLIST, arr => {
2208
- const raw = arr[0], items = raw.replace(/^ {0,3}/gm, '').split(LISTITEM).filter(i => !NullChecker.isNullOrEmpty(i)).map(i => i.trim());
3053
+ const raw = arr[0], startMatch = /\d+/.exec(raw), start = parseInt(startMatch[0]), tokens = tokenizeListItems(raw, lexer);
2209
3054
  return [{
2210
- type: MARKDOWN_TYPES.OrderedList, start: parseInt(/\d/.exec(arr[0])[0]), raw, text: raw, tokens: items.map(i => {
2211
- // avoid paragraphs if not strictly needed
2212
- return {
2213
- type: MARKDOWN_TYPES.ListItem, raw: i, text: i, tokens: tokenizeListItem(i, lexer)
2214
- };
2215
- })
3055
+ type: MARKDOWN_TYPES.OrderedList, start, raw, text: raw, tokens
2216
3056
  }];
2217
3057
  })
2218
3058
  },
2219
3059
  [MARKDOWN_RULES.UnorderedList]: {
2220
3060
  exec: (input, lexer) => match(input, ULIST, arr => {
2221
- const raw = arr[0], items = raw.replace(/^ {0,3}/gm, '').split(LISTITEM).filter(i => !NullChecker.isNullOrEmpty(i)).map(i => i.trim());
3061
+ const raw = arr[0], tokens = tokenizeListItems(raw, lexer);
2222
3062
  return [{
2223
- type: MARKDOWN_TYPES.UnorderedList, raw, text: raw, tokens: items.map(i => {
2224
- return {
2225
- type: MARKDOWN_TYPES.ListItem, raw: i, text: i, tokens: tokenizeListItem(i, lexer)
2226
- };
2227
- })
3063
+ type: MARKDOWN_TYPES.UnorderedList, raw, text: raw, tokens
3064
+ }];
3065
+ })
3066
+ },
3067
+ // GFM tables
3068
+ [MARKDOWN_RULES.Table]: {
3069
+ exec: (input, lexer) => match(input, TABLE, arr => {
3070
+ const headers = splitTableCells(arr[1]), delimiters = splitTableCells(arr[2]);
3071
+ // the delimiter row must match the header row
3072
+ if (headers.length !== delimiters.length) {
3073
+ return null;
3074
+ }
3075
+ const align = delimiters.map(tableAlignment);
3076
+ const bodyLines = arr[3].split(/\r?\n/).filter(l => l.trim().length > 0);
3077
+ const headerRow = tokenizeTableRow(headers, align, true, lexer);
3078
+ const bodyRows = bodyLines.map(line => {
3079
+ const cells = splitTableCells(line);
3080
+ return tokenizeTableRow(cells, align, false, lexer);
3081
+ });
3082
+ const rows = [headerRow].concat(bodyRows);
3083
+ return [{
3084
+ type: MARKDOWN_TYPES.Table, raw: arr[0], text: arr[0], align, tokens: rows
2228
3085
  }];
2229
3086
  })
2230
3087
  }
@@ -2233,13 +3090,16 @@
2233
3090
  Array.prototype.push.apply(blockRules, blockExtra);
2234
3091
  blockRules.push(lowpriority[MARKDOWN_RULES.Paragraph], lowpriority[MARKDOWN_RULES.Space]);
2235
3092
  // #endregion
2236
- return [
3093
+ const topRules = [
2237
3094
  // comment first
2238
3095
  inert[MARKDOWN_RULES.Comment],
3096
+ // (before references: `[^id]: …` would match them too)
3097
+ inert[MARKDOWN_RULES.FootnoteDefinition],
2239
3098
  inert[MARKDOWN_RULES.Reference]
2240
3099
  ].concat(
2241
3100
  // block rules contain recursive inline rules (at least)
2242
3101
  blockRules);
3102
+ return topRules;
2243
3103
  }
2244
3104
  // #region HTML RENDERER HELPER
2245
3105
  // meant to help in unnecessary spaces replacement.
@@ -2248,11 +3108,29 @@
2248
3108
  function trim(input) {
2249
3109
  return input?.replace(TRIM_INSIDE, '\n').trim();
2250
3110
  }
2251
- // #endregion
3111
+ function titleAttribute(title) {
3112
+ if (NullChecker.isNullOrEmpty(title)) {
3113
+ return '';
3114
+ }
3115
+ const encoded = attributeEncode(title);
3116
+ return ` title="${encoded}"`;
3117
+ }
3118
+ function alignAttribute(align) {
3119
+ if (align == null) {
3120
+ return '';
3121
+ }
3122
+ return ` style="text-align: ${align}"`;
3123
+ }
2252
3124
  /** Renders a Markdown token tree, as produced by the grammar returned by {@link getGrammar}, into HTML markup. */
2253
3125
  class HtmlRenderer {
2254
3126
  constructor(_extraItemRender = (token, __) => token?.text || '') {
2255
3127
  this._extraItemRender = _extraItemRender;
3128
+ /** Link/image reference definitions of the whole document (they may be referenced from any depth). */
3129
+ this.#references = [];
3130
+ /** Footnote definitions by id, and the order of their first reference (their numbering). */
3131
+ this.#footnotes = new Map();
3132
+ this.#footnoteOrder = [];
3133
+ this.#highlighter = new HighlightRenderer();
2256
3134
  this._renderContent = (token) => {
2257
3135
  const children = token.tokens;
2258
3136
  if (NullChecker.isNullOrEmpty(children)) {
@@ -2261,10 +3139,144 @@
2261
3139
  return this._render(children);
2262
3140
  };
2263
3141
  }
3142
+ /** Link/image reference definitions of the whole document (they may be referenced from any depth). */
3143
+ #references;
3144
+ /** Footnote definitions by id, and the order of their first reference (their numbering). */
3145
+ #footnotes;
3146
+ #footnoteOrder;
3147
+ #highlighter;
3148
+ /** Collects the reference and footnote definitions of the whole tree. */
3149
+ _collectDefinitions(tree) {
3150
+ for (let token of tree ?? []) {
3151
+ if (token?.type === MARKDOWN_TYPES.Reference) {
3152
+ this.#references.push(token);
3153
+ }
3154
+ if (token?.type === MARKDOWN_TYPES.FootnoteDefinition && !this.#footnotes.has(token['id'])) {
3155
+ this.#footnotes.set(token['id'], token);
3156
+ }
3157
+ this._collectDefinitions(token?.tokens);
3158
+ }
3159
+ }
3160
+ _findReference(ref, image) {
3161
+ if (NullChecker.isNullOrEmpty(ref)) {
3162
+ return undefined;
3163
+ }
3164
+ const key = ref.toLowerCase();
3165
+ return this.#references.find(t => (t.image === true) === image && t.id?.toLowerCase() === key);
3166
+ }
3167
+ _renderCodeBlock(token) {
3168
+ const R = CARRIAGE_RETURN_PLACEHOLDER;
3169
+ const lang = token['lang'];
3170
+ if (token['highlighted'] !== true) {
3171
+ const classAttribute = lang ? (' class="' + lang + '"') : '';
3172
+ const content = this._renderContent(token);
3173
+ return `<pre>${R}<code${classAttribute}>${content}</code>${R}</pre>${R}`;
3174
+ }
3175
+ // `code-highlighted` (and `code-markup`) let the theme style any registered language
3176
+ const classes = [lang, 'code-highlighted'];
3177
+ if (token['syntax'] === MARKUP_SYNTAX) {
3178
+ classes.push('code-markup');
3179
+ }
3180
+ const classList = attributeEncode(classes.join(' '));
3181
+ const highlighted = this.#highlighter.render(token.tokens);
3182
+ return `<pre>${R}<code class="${classList}">${highlighted}</code>${R}</pre>${R}`;
3183
+ }
3184
+ _renderImage(token) {
3185
+ let src = token.src, title = token.title;
3186
+ if (!src) {
3187
+ const reference = this._findReference(token.ref, true);
3188
+ src = reference?.url;
3189
+ title = reference?.title;
3190
+ }
3191
+ if (!src) {
3192
+ const content = this._renderContent(token);
3193
+ return `![${content}]`;
3194
+ }
3195
+ const titleAttr = titleAttribute(title);
3196
+ return `<img src="${src}" alt="${token.text}"${titleAttr} />`;
3197
+ }
3198
+ _renderLink(token) {
3199
+ let href = token.href, title = token.title;
3200
+ if (!href) {
3201
+ const reference = this._findReference(token.ref, false);
3202
+ href = reference?.url;
3203
+ title = reference?.title;
3204
+ }
3205
+ const content = this._renderContent(token);
3206
+ if (!href) {
3207
+ return `[${content}]`;
3208
+ }
3209
+ const titleAttr = titleAttribute(title);
3210
+ return `<a href="${href}"${titleAttr}>${content}</a>`;
3211
+ }
3212
+ _renderListItem(token) {
3213
+ const R = CARRIAGE_RETURN_PLACEHOLDER;
3214
+ const content = this._renderContent(token);
3215
+ if (token['task'] !== true) {
3216
+ return `\t<li>${content}</li>${R}`;
3217
+ }
3218
+ const checked = token['checked'] === true ? ' checked' : '';
3219
+ return `\t<li class="task-list-item"><input type="checkbox" disabled${checked} /> ${content}</li>${R}`;
3220
+ }
3221
+ _renderTableRow(row) {
3222
+ const R = CARRIAGE_RETURN_PLACEHOLDER;
3223
+ let cells = '';
3224
+ for (let cell of row.tokens ?? []) {
3225
+ const tag = cell['header'] === true ? 'th' : 'td';
3226
+ const align = alignAttribute(cell['align']);
3227
+ const content = this._renderContent(cell);
3228
+ cells += `<${tag}${align}>${content}</${tag}>`;
3229
+ }
3230
+ return `<tr>${cells}</tr>${R}`;
3231
+ }
3232
+ _renderTable(token) {
3233
+ const R = CARRIAGE_RETURN_PLACEHOLDER;
3234
+ const rows = token.tokens ?? [];
3235
+ const headerRows = rows.filter(r => r['header'] === true);
3236
+ const bodyRows = rows.filter(r => r['header'] !== true);
3237
+ const head = headerRows.map(r => this._renderTableRow(r)).join('');
3238
+ let html = `<table>${R}<thead>${R}${head}</thead>${R}`;
3239
+ if (bodyRows.length > 0) {
3240
+ const body = bodyRows.map(r => this._renderTableRow(r)).join('');
3241
+ html += `<tbody>${R}${body}</tbody>${R}`;
3242
+ }
3243
+ return html + `</table>${R}`;
3244
+ }
3245
+ _renderFootnoteReference(token) {
3246
+ const id = token['id'];
3247
+ if (!this.#footnotes.has(id)) {
3248
+ return `[^${token.text}]`;
3249
+ }
3250
+ const order = this.#footnoteOrder;
3251
+ const isFirst = !order.includes(id);
3252
+ if (isFirst) {
3253
+ order.push(id);
3254
+ }
3255
+ const number = order.indexOf(id) + 1;
3256
+ const anchorId = isFirst ? ` id="fnref-${id}"` : '';
3257
+ return `<sup class="footnote-ref"><a href="#fn-${id}"${anchorId}>${number}</a></sup>`;
3258
+ }
3259
+ /** Renders the referenced footnotes, in order of first reference (definitions may reference further footnotes). */
3260
+ _renderFootnotes() {
3261
+ const order = this.#footnoteOrder;
3262
+ if (order.length === 0) {
3263
+ return '';
3264
+ }
3265
+ const R = CARRIAGE_RETURN_PLACEHOLDER;
3266
+ let items = '';
3267
+ for (let i = 0; i < order.length; i++) {
3268
+ const id = order[i];
3269
+ const definition = this.#footnotes.get(id);
3270
+ const content = this._renderContent(definition);
3271
+ items += `<li id="fn-${id}">${content} <a href="#fnref-${id}" class="footnote-backref">&#8617;</a></li>${R}`;
3272
+ }
3273
+ return `<section class="footnotes">${R}<ol>${R}${items}</ol>${R}</section>${R}`;
3274
+ }
2264
3275
  _rendeItem(token, allTokens) {
2265
3276
  const r = this._renderContent, R = CARRIAGE_RETURN_PLACEHOLDER;
2266
3277
  switch (token?.type) {
2267
3278
  case MARKDOWN_TYPES.Reference:
3279
+ case MARKDOWN_TYPES.FootnoteDefinition:
2268
3280
  return '';
2269
3281
  case MARKDOWN_TYPES.Html:
2270
3282
  return token.raw;
@@ -2273,8 +3285,7 @@
2273
3285
  case MARKDOWN_TYPES.Bold:
2274
3286
  return `<b>${r(token)}</b>`;
2275
3287
  case MARKDOWN_TYPES.CodeBlock:
2276
- const lang = token['lang'];
2277
- return `<pre>${R}<code${(lang ? (' class="' + lang + '"') : '')}>${r(token)}</code>${R}</pre>${R}`;
3288
+ return this._renderCodeBlock(token);
2278
3289
  case MARKDOWN_TYPES.Code:
2279
3290
  return `<code>${r(token)}</code>`;
2280
3291
  case MARKDOWN_TYPES.HorizontalRule:
@@ -2284,17 +3295,7 @@
2284
3295
  return `<h${hlevel} id="${hid}">${r(token)}</h${hlevel}>${R}`;
2285
3296
  case MARKDOWN_TYPES.Image:
2286
3297
  case MARKDOWN_TYPES.ImageLike:
2287
- const itk = token;
2288
- let isrc = itk.src, ititle = '';
2289
- const alt = itk.text, iref = itk.ref;
2290
- if (!isrc && iref) {
2291
- const ireftk = allTokens.find(t => t.type === MARKDOWN_TYPES.Reference && t['image'] === true && t['id']?.toLowerCase() === iref.toLowerCase());
2292
- if (ireftk) {
2293
- isrc = ireftk.url;
2294
- ititle = ireftk.title ?? '';
2295
- }
2296
- }
2297
- return isrc ? `<img src="${isrc}" alt="${alt}"${(ititle ? (' title="' + ititle + '"') : '')} />` : `![${r(token)}]`;
3298
+ return this._renderImage(token);
2298
3299
  case MARKDOWN_TYPES.Italic:
2299
3300
  return `<i>${r(token)}</i>`;
2300
3301
  case MARKDOWN_TYPES.StrikeThrough:
@@ -2308,20 +3309,14 @@
2308
3309
  case MARKDOWN_TYPES.UnorderedList:
2309
3310
  return `<ul>\n${r(token)}</ul>${R}`;
2310
3311
  case MARKDOWN_TYPES.ListItem:
2311
- return `\t<li>${r(token)}</li>${R}`;
3312
+ return this._renderListItem(token);
2312
3313
  case MARKDOWN_TYPES.Link:
2313
3314
  case MARKDOWN_TYPES.LinkLike:
2314
- const atk = token;
2315
- let ahref = atk.href, atitle = '';
2316
- const aref = atk.ref;
2317
- if (!ahref && aref) {
2318
- const areftk = allTokens.find(t => t.type === MARKDOWN_TYPES.Reference && t['image'] !== true && t['id']?.toLowerCase() === aref.toLowerCase());
2319
- if (areftk) {
2320
- ahref = areftk.url;
2321
- atitle = areftk.title ?? '';
2322
- }
2323
- }
2324
- return ahref ? `<a href="${ahref}"${(atitle ? (' title="' + atitle + '"') : '')}>${r(token)}</a>` : `[${r(token)}]`;
3315
+ return this._renderLink(token);
3316
+ case MARKDOWN_TYPES.Table:
3317
+ return this._renderTable(token);
3318
+ case MARKDOWN_TYPES.FootnoteReference:
3319
+ return this._renderFootnoteReference(token);
2325
3320
  case MARKDOWN_TYPES.Paragraph:
2326
3321
  return `<p>${r(token)}</p>${R}`;
2327
3322
  case MARKDOWN_TYPES.Text:
@@ -2338,7 +3333,13 @@
2338
3333
  return acc;
2339
3334
  }
2340
3335
  render(tree) {
2341
- return trim(this._render(tree));
3336
+ this.#references = [];
3337
+ this.#footnotes.clear();
3338
+ this.#footnoteOrder = [];
3339
+ this._collectDefinitions(tree);
3340
+ const body = this._render(tree);
3341
+ const footnotes = this._renderFootnotes();
3342
+ return trim(body + footnotes);
2342
3343
  }
2343
3344
  }
2344
3345
  /** Renders a Markdown token tree as a JSON array of tokens, mainly useful for debugging/inspection. */
@@ -2380,12 +3381,13 @@
2380
3381
  return JSON.parse(json);
2381
3382
  }
2382
3383
  parse(input, grammarOrRenderer = getGrammar(), renderer = new HtmlRenderer()) {
3384
+ // line endings are normalized: the grammar expects `\n` (`.` doesn't match `\r`: CRLF paragraphs would split by line)
3385
+ const normalized = (input ?? '').replace(/\r\n?/g, '\n');
2383
3386
  if (isRenderer(grammarOrRenderer)) {
2384
- return super.parse(input, getGrammar(), grammarOrRenderer);
2385
- }
2386
- else {
2387
- return super.parse(input, grammarOrRenderer, renderer);
3387
+ const grammar = getGrammar();
3388
+ return super.parse(normalized, grammar, grammarOrRenderer);
2388
3389
  }
3390
+ return super.parse(normalized, grammarOrRenderer, renderer);
2389
3391
  }
2390
3392
  }
2391
3393
  //}
@@ -2395,10 +3397,357 @@
2395
3397
  Parser: Parser
2396
3398
  });
2397
3399
 
3400
+ //namespace Pacem.Compile {
3401
+ /** After these, a `/` starts a division (not a regular expression literal). */
3402
+ const DIVISION_AFTER_TYPES = ['identifier', 'number', 'string', 'regex'];
3403
+ const DIVISION_AFTER_TEXTS = [')', ']', '}', 'this', 'super', 'null', 'undefined', 'true', 'false', '++', '--'];
3404
+ const JAVASCRIPT_KEYWORDS = [
3405
+ // JavaScript
3406
+ 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
3407
+ 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'from', 'function', 'get', 'if', 'import', 'in',
3408
+ 'instanceof', 'let', 'new', 'null', 'of', 'return', 'set', 'static', 'super', 'switch', 'this', 'throw', 'true',
3409
+ 'try', 'typeof', 'undefined', 'var', 'void', 'while', 'with', 'yield',
3410
+ // TypeScript
3411
+ 'abstract', 'accessor', 'any', 'as', 'asserts', 'bigint', 'boolean', 'declare', 'implements', 'infer',
3412
+ 'interface', 'is', 'keyof', 'module', 'namespace', 'never', 'number', 'object', 'override', 'package', 'private',
3413
+ 'protected', 'public', 'readonly', 'satisfies', 'string', 'symbol', 'type', 'unique', 'unknown'
3414
+ ];
3415
+ /** Rules shared by every "code" state (top level, template expressions, nested braces). */
3416
+ const CODE_RULES$1 = [
3417
+ { match: String.raw `/\*\*(?!/)[\s\S]*?(?:\*/|$)`, type: 'doc-comment' },
3418
+ { match: String.raw `/\*[\s\S]*?(?:\*/|$)`, type: 'comment' },
3419
+ { match: String.raw `//[^\r\n]*`, type: 'comment' },
3420
+ { match: String.raw `\r\n|[\r\n

]`, type: 'newline' },
3421
+ { match: String.raw `[ \t\f\v ]+`, type: 'space' },
3422
+ { match: '`', type: 'string', push: 'template' },
3423
+ { match: String.raw `"(?:\\[\s\S]|[^"\\\r\n])*"?`, type: 'string' },
3424
+ { match: String.raw `'(?:\\[\s\S]|[^'\\\r\n])*'?`, type: 'string' },
3425
+ {
3426
+ // regular expression literal (only where a division can't be)
3427
+ match: String.raw `/(?![*/])(?:\\.|\[(?:\\.|[^\]\\\r\n])*\]|[^/\\\[\r\n])+/[a-z]*`, type: 'regex',
3428
+ when: { previousNotTypes: DIVISION_AFTER_TYPES, previousNotTexts: DIVISION_AFTER_TEXTS }
3429
+ },
3430
+ { match: String.raw `0[xX][\da-fA-F_]+n?|0[bB][01_]+n?|0[oO][0-7_]+n?|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][+-]?\d[\d_]*)?n?`, type: 'number' },
3431
+ { match: String.raw `#?[A-Za-z_$ -￿][\w$ -￿]*`, type: 'identifier' },
3432
+ { match: String.raw `[()\[\]{}]`, type: 'bracket' },
3433
+ { match: String.raw `\?\.(?!\d)|\.\.\.|[;,.]`, type: 'punctuation' },
3434
+ { match: String.raw `>>>=|<<=|>>=|>>>|\*\*=|&&=|\|\|=|\?\?=|===|!==|=>|\*\*|\+\+|--|<<|>>|&&|\|\||\?\?|[-+*/%&|^<>!=]=?|[~?:@]`, type: 'operator' },
3435
+ ];
3436
+ /**
3437
+ * JavaScript vocabulary, TypeScript superset: comments (JSDoc as `doc-comment`), strings, template literals with nested
3438
+ * `${…}` expressions, regular expression literals (told from divisions by the previous significant token), numbers
3439
+ * (hex/binary/octal/BigInt/separators), identifiers (`#private` included), keywords (JS + TS, not after `.`/`?.`),
3440
+ * brackets, punctuation and operators.
3441
+ */
3442
+ const JavaScriptSyntax = {
3443
+ name: 'javascript',
3444
+ aliases: ['js', 'mjs', 'cjs', 'jsx', 'typescript', 'ts', 'tsx', 'script', 'ecmascript'],
3445
+ lexical: {
3446
+ start: 'root',
3447
+ states: {
3448
+ root: CODE_RULES$1,
3449
+ template: [
3450
+ { match: '`', type: 'string', pop: true },
3451
+ { match: String.raw `\$\{`, type: 'punctuation', push: 'templateExpression' },
3452
+ { match: String.raw `(?:\\[\s\S]|\$(?!\{)|[^\\` + '`' + String.raw `$])+`, type: 'string' },
3453
+ ],
3454
+ templateExpression: [
3455
+ { match: String.raw `\}`, type: 'punctuation', pop: true },
3456
+ { match: String.raw `\{`, type: 'bracket', push: 'braces' },
3457
+ { include: 'root' },
3458
+ ],
3459
+ braces: [
3460
+ { match: String.raw `\}`, type: 'bracket', pop: true },
3461
+ { match: String.raw `\{`, type: 'bracket', push: 'braces' },
3462
+ { include: 'root' },
3463
+ ],
3464
+ },
3465
+ keywords: { keyword: JAVASCRIPT_KEYWORDS },
3466
+ keywordExceptAfter: ['.', '?.'],
3467
+ }
3468
+ };
3469
+
3470
+ //namespace Pacem.Compile {
3471
+ /** Member accesses (`obj.#x`, `obj?.x`) are never Pacem binding tokens. */
3472
+ const NOT_AFTER_MEMBER_ACCESS = { previousNotTexts: ['.', '?.'] };
3473
+ /**
3474
+ * Pacem binding-expression vocabulary (the content of `{{ … }}` attributes): JavaScript plus the special tokens
3475
+ * resolved by core's `Expression`:
3476
+ * - `binding-host`: `:host` (followed by `.`);
3477
+ * - `binding-host-property`: `::name`;
3478
+ * - `binding-item`: `^item`, `^^item`, … (repeater item, n levels up);
3479
+ * - `binding-index`: `^index`, `^^index`, …;
3480
+ * - `binding-element`: `#id` (element reference);
3481
+ * - `binding-this`: `$this` / `this` (the bound element).
3482
+ */
3483
+ const PacemBindingSyntax = {
3484
+ name: 'pacem-binding',
3485
+ aliases: ['pacem', 'binding'],
3486
+ extends: 'javascript',
3487
+ lexical: {
3488
+ states: {
3489
+ root: [
3490
+ { match: String.raw `:host(?=\.)`, type: 'binding-host', when: NOT_AFTER_MEMBER_ACCESS },
3491
+ { match: String.raw `::[A-Za-z_$][\w$]*`, type: 'binding-host-property' },
3492
+ { match: String.raw `\^+item(?![\w$])`, type: 'binding-item' },
3493
+ { match: String.raw `\^+index(?![\w$])`, type: 'binding-index' },
3494
+ { match: String.raw `#[A-Za-z_][\w]*`, type: 'binding-element', when: NOT_AFTER_MEMBER_ACCESS },
3495
+ { match: String.raw `\$?this(?![\w$])`, type: 'binding-this', when: NOT_AFTER_MEMBER_ACCESS },
3496
+ ]
3497
+ }
3498
+ }
3499
+ };
3500
+
3501
+ //namespace Pacem.Compile {
3502
+ const NEWLINE_RULE = { match: String.raw `\r\n|[\r\n]`, type: 'newline' };
3503
+ /** Attribute list of an opening tag. */
3504
+ const ATTRIBUTE_RULES = [
3505
+ NEWLINE_RULE,
3506
+ { match: String.raw `[ \t\f]+`, type: 'space' },
3507
+ { match: '"', type: 'string', push: 'doubleQuoted' },
3508
+ { match: "'", type: 'string', push: 'singleQuoted' },
3509
+ { match: String.raw `[^\s=/>"']+`, type: 'attribute' },
3510
+ { match: '=', type: 'operator' },
3511
+ { match: '/', type: 'punctuation' },
3512
+ ];
3513
+ /** A Pacem binding (`{{ … }}`) inside an attribute value: embeds the `pacem-binding` vocabulary up to `}}`. */
3514
+ const BINDING_RULES = [
3515
+ { match: String.raw `\{\{`, type: 'punctuation', embed: 'pacem-binding', until: String.raw `\}\}` },
3516
+ { match: String.raw `\}\}`, type: 'punctuation' },
3517
+ ];
3518
+ /**
3519
+ * Markup (HTML/XML) vocabulary: comments, doctypes/CDATA, tags (`tag`/`tagname`), attributes, quoted values (Pacem
3520
+ * `{{ … }}` bindings tokenized with `pacem-binding`), entities, text, and `<script>` bodies tokenized with `javascript`.
3521
+ */
3522
+ const MarkupSyntax = {
3523
+ name: 'markup',
3524
+ aliases: ['html', 'htm', 'xml', 'xhtml', 'svg'],
3525
+ lexical: {
3526
+ start: 'root',
3527
+ states: {
3528
+ root: [
3529
+ { match: String.raw `<!--[\s\S]*?(?:-->|$)`, type: 'comment' },
3530
+ { match: String.raw `<![^>]*>?`, type: 'tag' },
3531
+ { match: String.raw `(<\/)([\w:.-]+)(\s*>?)`, type: ['tag', 'tagname', 'tag'] },
3532
+ { match: String.raw `(<)(script)(?![\w:.-])`, flags: 'i', type: ['tag', 'tagname'], push: 'scriptTag' },
3533
+ { match: String.raw `(<)([\w:.-]+)`, type: ['tag', 'tagname'], push: 'tag' },
3534
+ { match: String.raw `&(?:#\d+|#[xX][\da-fA-F]+|\w+);`, type: 'entity' },
3535
+ NEWLINE_RULE,
3536
+ { match: String.raw `[^<&\r\n]+`, type: 'text' },
3537
+ ],
3538
+ tag: [
3539
+ { match: String.raw `/?>`, type: 'tag', pop: true },
3540
+ { include: 'attributes' },
3541
+ ],
3542
+ scriptTag: [
3543
+ { match: '/>', type: 'tag', pop: true },
3544
+ // the script body is tokenized as JavaScript, up to the closing tag
3545
+ { match: '>', type: 'tag', pop: true, embed: 'javascript', until: String.raw `<\/script` },
3546
+ { include: 'attributes' },
3547
+ ],
3548
+ attributes: ATTRIBUTE_RULES,
3549
+ doubleQuoted: [
3550
+ { match: '"', type: 'string', pop: true },
3551
+ ...BINDING_RULES,
3552
+ { match: String.raw `(?:[^"{}]|\{(?!\{)|\}(?!\}))+`, type: 'string' },
3553
+ ],
3554
+ singleQuoted: [
3555
+ { match: "'", type: 'string', pop: true },
3556
+ ...BINDING_RULES,
3557
+ { match: String.raw `(?:[^'{}]|\{(?!\{)|\}(?!\}))+`, type: 'string' },
3558
+ ],
3559
+ },
3560
+ insignificant: ['space', 'newline', 'comment', 'text']
3561
+ }
3562
+ };
3563
+
3564
+ //namespace Pacem.Compile {
3565
+ const CSHARP_KEYWORDS = [
3566
+ 'abstract', 'add', 'alias', 'and', 'args', 'as', 'ascending', 'async', 'await', 'base', 'bool', 'break', 'by',
3567
+ 'byte', 'case', 'catch', 'char', 'checked', 'class', 'const', 'continue', 'decimal', 'default', 'delegate',
3568
+ 'descending', 'do', 'double', 'dynamic', 'else', 'enum', 'equals', 'event', 'explicit', 'extern', 'false', 'file',
3569
+ 'finally', 'fixed', 'float', 'for', 'foreach', 'from', 'get', 'global', 'goto', 'group', 'if', 'implicit', 'in',
3570
+ 'init', 'int', 'interface', 'internal', 'into', 'is', 'join', 'let', 'lock', 'long', 'managed', 'nameof',
3571
+ 'namespace', 'new', 'nint', 'not', 'notnull', 'nuint', 'null', 'object', 'on', 'operator', 'or', 'orderby', 'out',
3572
+ 'override', 'params', 'partial', 'private', 'protected', 'public', 'readonly', 'record', 'ref', 'remove',
3573
+ 'required', 'return', 'sbyte', 'scoped', 'sealed', 'select', 'set', 'short', 'sizeof', 'stackalloc', 'static',
3574
+ 'string', 'struct', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong', 'unchecked', 'unmanaged',
3575
+ 'unsafe', 'ushort', 'using', 'value', 'var', 'virtual', 'void', 'volatile', 'when', 'where', 'while', 'with',
3576
+ 'yield'
3577
+ ];
3578
+ /** Rules shared by every "code" state (top level, interpolations, nested braces). */
3579
+ const CODE_RULES = [
3580
+ { match: String.raw `///[^\r\n]*`, type: 'doc-comment' },
3581
+ { match: String.raw `//[^\r\n]*`, type: 'comment' },
3582
+ { match: String.raw `/\*[\s\S]*?(?:\*/|$)`, type: 'comment' },
3583
+ // preprocessor directives, at the beginning of a line
3584
+ { match: String.raw `(?<=^[ \t]*|[\r\n][ \t]*)#[ \t]*[a-z]+[^\r\n]*`, type: 'preprocessor' },
3585
+ { match: String.raw `\r\n|[\r\n]`, type: 'newline' },
3586
+ { match: String.raw `[ \t\f\v\u00A0\uFEFF]+`, type: 'space' },
3587
+ // raw string literals ("""…""", optionally interpolated)
3588
+ { match: String.raw `\$*("{3,})[\s\S]*?\1`, type: 'string' },
3589
+ { match: String.raw `\$@"|@\$"`, type: 'string', push: 'interpolatedVerbatim' },
3590
+ { match: String.raw `\$"`, type: 'string', push: 'interpolated' },
3591
+ { match: String.raw `@"(?:[^"]|"")*"?`, type: 'string' },
3592
+ { match: String.raw `"(?:\\.|[^"\\\r\n])*"?`, type: 'string' },
3593
+ { match: String.raw `'(?:\\.|[^'\\\r\n])*'?`, type: 'string' },
3594
+ { match: String.raw `0[xX][\da-fA-F_]+[uUlL]*|0[bB][01_]+[uUlL]*|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:[eE][+-]?\d+)?[fFdDmMuUlL]*`, type: 'number' },
3595
+ { match: String.raw `@?[A-Za-z_\u00A0-\uFFFF][\w\u00A0-\uFFFF]*`, type: 'identifier' },
3596
+ { match: String.raw `[()\[\]{}]`, type: 'bracket' },
3597
+ { match: String.raw `\?\.|::|[;,.]`, type: 'punctuation' },
3598
+ { match: String.raw `\?\?=|<<=|>>>=|>>=|>>>|=>|==|!=|<=|>=|&&|\|\||\?\?|\+\+|--|->|<<|[-+*/%&|^<>!=]=?|[~?:]`, type: 'operator' },
3599
+ ];
3600
+ /**
3601
+ * C# vocabulary: XML doc comments (`doc-comment`), comments, preprocessor directives, strings (regular, verbatim, raw,
3602
+ * interpolated with nested `{…}` expressions), chars, numbers (with suffixes), identifiers (`@verbatim` included),
3603
+ * keywords (contextual ones included, not after `.`/`?.`), brackets, punctuation and operators.
3604
+ */
3605
+ const CSharpSyntax = {
3606
+ name: 'csharp',
3607
+ aliases: ['cs', 'c#', 'c-sharp', 'dotnet'],
3608
+ lexical: {
3609
+ start: 'root',
3610
+ states: {
3611
+ root: CODE_RULES,
3612
+ interpolated: [
3613
+ { match: '"', type: 'string', pop: true },
3614
+ { match: String.raw `\{\{|\}\}`, type: 'string' },
3615
+ { match: String.raw `\{`, type: 'punctuation', push: 'interpolation' },
3616
+ { match: String.raw `(?:\\.|[^"\\{}\r\n])+`, type: 'string' },
3617
+ ],
3618
+ interpolatedVerbatim: [
3619
+ { match: '""', type: 'string' },
3620
+ { match: '"', type: 'string', pop: true },
3621
+ { match: String.raw `\{\{|\}\}`, type: 'string' },
3622
+ { match: String.raw `\{`, type: 'punctuation', push: 'interpolation' },
3623
+ { match: String.raw `[^"{}]+`, type: 'string' },
3624
+ ],
3625
+ interpolation: [
3626
+ { match: String.raw `\}`, type: 'punctuation', pop: true },
3627
+ { match: String.raw `\{`, type: 'bracket', push: 'braces' },
3628
+ { include: 'root' },
3629
+ ],
3630
+ braces: [
3631
+ { match: String.raw `\}`, type: 'bracket', pop: true },
3632
+ { match: String.raw `\{`, type: 'bracket', push: 'braces' },
3633
+ { include: 'root' },
3634
+ ],
3635
+ },
3636
+ keywords: { keyword: CSHARP_KEYWORDS },
3637
+ keywordExceptAfter: ['.', '?.'],
3638
+ }
3639
+ };
3640
+
3641
+ //namespace Pacem.Compile {
3642
+ const C_KEYWORDS = [
3643
+ // C
3644
+ 'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum', 'extern', 'float',
3645
+ 'for', 'goto', 'if', 'inline', 'int', 'long', 'register', 'restrict', 'return', 'short', 'signed', 'sizeof',
3646
+ 'static', 'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while', 'bool', 'true', 'false',
3647
+ 'NULL', '_Bool', '_Complex', '_Imaginary', '_Alignas', '_Alignof', '_Atomic', '_Generic', '_Noreturn',
3648
+ '_Static_assert', '_Thread_local',
3649
+ // C++
3650
+ 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'bitand', 'bitor', 'catch', 'char8_t', 'char16_t', 'char32_t',
3651
+ 'class', 'co_await', 'co_return', 'co_yield', 'compl', 'concept', 'consteval', 'constexpr', 'constinit',
3652
+ 'const_cast', 'decltype', 'delete', 'dynamic_cast', 'explicit', 'export', 'final', 'friend', 'mutable',
3653
+ 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr', 'operator', 'or', 'or_eq', 'override', 'private',
3654
+ 'protected', 'public', 'reinterpret_cast', 'requires', 'static_assert', 'static_cast', 'template', 'this',
3655
+ 'thread_local', 'throw', 'try', 'typeid', 'typename', 'using', 'virtual', 'wchar_t', 'xor', 'xor_eq'
3656
+ ];
3657
+ /**
3658
+ * C/C++ vocabulary: comments (`/** … *\/` and `///` as `doc-comment`), preprocessor directives (with their `<header>`),
3659
+ * strings (raw `R"x(…)x"` and prefixed ones included), chars, numbers (with suffixes/separators), identifiers, keywords
3660
+ * (C and C++), brackets, punctuation and operators.
3661
+ */
3662
+ const CSyntax = {
3663
+ name: 'c',
3664
+ aliases: ['cpp', 'c++', 'cc', 'cxx', 'h', 'hpp', 'hxx', 'objc'],
3665
+ lexical: {
3666
+ start: 'root',
3667
+ states: {
3668
+ root: [
3669
+ { match: String.raw `/\*\*(?!/)[\s\S]*?(?:\*/|$)|///[^\r\n]*`, type: 'doc-comment' },
3670
+ { match: String.raw `/\*[\s\S]*?(?:\*/|$)`, type: 'comment' },
3671
+ { match: String.raw `//[^\r\n]*`, type: 'comment' },
3672
+ // preprocessor directives, at the beginning of a line (line continuations included)
3673
+ { match: String.raw `(?<=^[ \t]*|[\r\n][ \t]*)#[ \t]*\w+(?:\\\r?\n|[^\r\n])*`, type: 'preprocessor' },
3674
+ { match: String.raw `\r\n|[\r\n]`, type: 'newline' },
3675
+ { match: String.raw `[ \t\f\v]+`, type: 'space' },
3676
+ { match: String.raw `(?:u8|u|U|L)?R"([^()\\\s]{0,16})\([\s\S]*?\)\1"`, type: 'string' },
3677
+ { match: String.raw `(?:u8|u|U|L)?"(?:\\[\s\S]|[^"\\\r\n])*"?`, type: 'string' },
3678
+ { match: String.raw `(?:u8|u|U|L)?'(?:\\.|[^'\\\r\n])*'?`, type: 'string' },
3679
+ { match: String.raw `0[xX][\da-fA-F']+[uUlL]*|0[bB][01']+[uUlL]*|(?:\d[\d']*\.?[\d']*|\.\d[\d']*)(?:[eEpP][+-]?\d+)?[fFlLuU]*`, type: 'number' },
3680
+ { match: String.raw `[A-Za-z_][\w]*`, type: 'identifier' },
3681
+ { match: String.raw `[()\[\]{}]`, type: 'bracket' },
3682
+ { match: String.raw `::|\.\.\.|->\*?|\.\*?|[;,]`, type: 'punctuation' },
3683
+ { match: String.raw `<=>|<<=|>>=|<<|>>|\+\+|--|&&|\|\||[-+*/%&|^<>!=]=?|[~?:#]`, type: 'operator' },
3684
+ ],
3685
+ },
3686
+ keywords: { keyword: C_KEYWORDS },
3687
+ keywordExceptAfter: ['.', '->', '::'],
3688
+ }
3689
+ };
3690
+
3691
+ //namespace Pacem.Compile {
3692
+ /**
3693
+ * JSON vocabulary (JSONC and JSON5-ish tolerant: comments, single quotes and trailing commas are recognized):
3694
+ * object keys are `attribute`s, values `string`/`number`/`keyword` (`true`, `false`, `null`).
3695
+ */
3696
+ const JsonSyntax = {
3697
+ name: 'json',
3698
+ aliases: ['jsonc', 'json5', 'webmanifest'],
3699
+ lexical: {
3700
+ start: 'root',
3701
+ states: {
3702
+ root: [
3703
+ { match: String.raw `/\*[\s\S]*?(?:\*/|$)`, type: 'comment' },
3704
+ { match: String.raw `//[^\r\n]*`, type: 'comment' },
3705
+ { match: String.raw `\r\n|[\r\n]`, type: 'newline' },
3706
+ { match: String.raw `[ \t\f\v ]+`, type: 'space' },
3707
+ // keys: strings followed by a colon
3708
+ { match: String.raw `(?:"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*')(?=\s*:)`, type: 'attribute' },
3709
+ { match: String.raw `"(?:\\.|[^"\\\r\n])*"?|'(?:\\.|[^'\\\r\n])*'?`, type: 'string' },
3710
+ { match: String.raw `-?(?:0[xX][\da-fA-F]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)|-?Infinity|NaN`, type: 'number' },
3711
+ { match: String.raw `[A-Za-z_$][\w$]*`, type: 'identifier' },
3712
+ { match: String.raw `[\[\]{}]`, type: 'bracket' },
3713
+ { match: String.raw `[:,]`, type: 'punctuation' },
3714
+ ],
3715
+ },
3716
+ keywords: { keyword: ['true', 'false', 'null'] },
3717
+ }
3718
+ };
3719
+
3720
+ //namespace Pacem.Compile {
3721
+ /** The built-in vocabularies, one per {@link SyntaxName} (enforced by the type). */
3722
+ const BUILTIN_SYNTAXES = {
3723
+ 'javascript': JavaScriptSyntax,
3724
+ 'pacem-binding': PacemBindingSyntax,
3725
+ 'markup': MarkupSyntax,
3726
+ 'csharp': CSharpSyntax,
3727
+ 'c': CSyntax,
3728
+ 'json': JsonSyntax
3729
+ };
3730
+ // available as soon as the compile module loads
3731
+ for (let name in BUILTIN_SYNTAXES) {
3732
+ const syntax = BUILTIN_SYNTAXES[name];
3733
+ Syntaxes.register(syntax);
3734
+ }
3735
+
2398
3736
  var indexCompile = /*#__PURE__*/Object.freeze({
2399
3737
  __proto__: null,
3738
+ CSharpSyntax: CSharpSyntax,
3739
+ CSyntax: CSyntax,
3740
+ DefaultHighlightClasses: DefaultHighlightClasses,
3741
+ HighlightRenderer: HighlightRenderer,
3742
+ JavaScriptSyntax: JavaScriptSyntax,
3743
+ JsonSyntax: JsonSyntax,
2400
3744
  Markdown: indexCompileMarkdown,
2401
- Parser: Parser$1
3745
+ MarkupSyntax: MarkupSyntax,
3746
+ PacemBindingSyntax: PacemBindingSyntax,
3747
+ Parser: Parser$1,
3748
+ Scanner: Scanner,
3749
+ get SyntaxTokenType () { return SyntaxTokenType; },
3750
+ Syntaxes: Syntaxes
2402
3751
  });
2403
3752
 
2404
3753
  var Output = /*#__PURE__*/Object.freeze({