mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-22 22:02:59 +00:00
@@ -0,0 +1,46 @@
|
||||
import { escapeHTML } from './utils';
|
||||
|
||||
const SPAN_CLOSE = '</span>';
|
||||
const emitsWrappingTags = (node) => {
|
||||
return !!node.kind;
|
||||
};
|
||||
|
||||
export default class HTMLRenderer {
|
||||
constructor(tree, options) {
|
||||
this.buffer = "";
|
||||
this.classPrefix = options.classPrefix;
|
||||
tree.walk(this);
|
||||
}
|
||||
|
||||
// renderer API
|
||||
|
||||
addText(text) {
|
||||
this.buffer += escapeHTML(text);
|
||||
}
|
||||
|
||||
openNode(node) {
|
||||
if (!emitsWrappingTags(node)) return;
|
||||
|
||||
let className = node.kind;
|
||||
if (!node.sublanguage) {
|
||||
className = `${this.classPrefix}${className}`;
|
||||
}
|
||||
this.span(className);
|
||||
}
|
||||
|
||||
closeNode(node) {
|
||||
if (!emitsWrappingTags(node)) return;
|
||||
|
||||
this.buffer += SPAN_CLOSE;
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
span(className) {
|
||||
this.buffer += `<span class="${className}">`;
|
||||
}
|
||||
|
||||
value() {
|
||||
return this.buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import * as regex from './regex';
|
||||
import { inherit } from './utils';
|
||||
|
||||
// keywords that should have no default relevance value
|
||||
var COMMON_KEYWORDS = 'of and for in not or if then'.split(' ');
|
||||
|
||||
// compilation
|
||||
|
||||
export function compileLanguage(language) {
|
||||
|
||||
function langRe(value, global) {
|
||||
return new RegExp(
|
||||
regex.source(value),
|
||||
'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
Stores multiple regular expressions and allows you to quickly search for
|
||||
them all in a string simultaneously - returning the first match. It does
|
||||
this by creating a huge (a|b|c) regex - each individual item wrapped with ()
|
||||
and joined by `|` - using match groups to track position. When a match is
|
||||
found checking which position in the array has content allows us to figure
|
||||
out which of the original regexes / match groups triggered the match.
|
||||
|
||||
The match object itself (the result of `Regex.exec`) is returned but also
|
||||
enhanced by merging in any meta-data that was registered with the regex.
|
||||
This is how we keep track of which mode matched, and what type of rule
|
||||
(`illegal`, `begin`, end, etc).
|
||||
*/
|
||||
class MultiRegex {
|
||||
constructor() {
|
||||
this.matchIndexes = {};
|
||||
this.regexes = [];
|
||||
this.matchAt = 1;
|
||||
this.position = 0;
|
||||
}
|
||||
|
||||
addRule(re, opts) {
|
||||
opts.position = this.position++;
|
||||
this.matchIndexes[this.matchAt] = opts;
|
||||
this.regexes.push([opts, re]);
|
||||
this.matchAt += regex.countMatchGroups(re) + 1;
|
||||
}
|
||||
|
||||
compile() {
|
||||
if (this.regexes.length === 0) {
|
||||
// avoids the need to check length every time exec is called
|
||||
this.exec = () => null;
|
||||
}
|
||||
const terminators = this.regexes.map(el => el[1]);
|
||||
this.matcherRe = langRe(regex.join(terminators, '|'), true);
|
||||
this.lastIndex = 0;
|
||||
}
|
||||
|
||||
exec(s) {
|
||||
this.matcherRe.lastIndex = this.lastIndex;
|
||||
const match = this.matcherRe.exec(s);
|
||||
if (!match) { return null; }
|
||||
|
||||
// eslint-disable-next-line no-undefined
|
||||
const i = match.findIndex((el, i) => i > 0 && el !== undefined);
|
||||
const matchData = this.matchIndexes[i];
|
||||
// trim off any earlier non-relevant match groups (ie, the other regex
|
||||
// match groups that make up the multi-matcher)
|
||||
match.splice(0, i);
|
||||
|
||||
return Object.assign(match, matchData);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Created to solve the key deficiently with MultiRegex - there is no way to
|
||||
test for multiple matches at a single location. Why would we need to do
|
||||
that? In the future a more dynamic engine will allow certain matches to be
|
||||
ignored. An example: if we matched say the 3rd regex in a large group but
|
||||
decided to ignore it - we'd need to started testing again at the 4th
|
||||
regex... but MultiRegex itself gives us no real way to do that.
|
||||
|
||||
So what this class creates MultiRegexs on the fly for whatever search
|
||||
position they are needed.
|
||||
|
||||
NOTE: These additional MultiRegex objects are created dynamically. For most
|
||||
grammars most of the time we will never actually need anything more than the
|
||||
first MultiRegex - so this shouldn't have too much overhead.
|
||||
|
||||
Say this is our search group, and we match regex3, but wish to ignore it.
|
||||
|
||||
regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0
|
||||
|
||||
What we need is a new MultiRegex that only includes the remaining
|
||||
possibilities:
|
||||
|
||||
regex4 | regex5 ' ie, startAt = 3
|
||||
|
||||
This class wraps all that complexity up in a simple API... `startAt` decides
|
||||
where in the array of expressions to start doing the matching. It
|
||||
auto-increments, so if a match is found at position 2, then startAt will be
|
||||
set to 3. If the end is reached startAt will return to 0.
|
||||
|
||||
MOST of the time the parser will be setting startAt manually to 0.
|
||||
*/
|
||||
class ResumableMultiRegex {
|
||||
constructor() {
|
||||
this.rules = [];
|
||||
this.multiRegexes = [];
|
||||
this.count = 0;
|
||||
|
||||
this.lastIndex = 0;
|
||||
this.regexIndex = 0;
|
||||
}
|
||||
|
||||
getMatcher(index) {
|
||||
if (this.multiRegexes[index]) return this.multiRegexes[index];
|
||||
|
||||
const matcher = new MultiRegex();
|
||||
this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts));
|
||||
matcher.compile();
|
||||
this.multiRegexes[index] = matcher;
|
||||
return matcher;
|
||||
}
|
||||
|
||||
considerAll() {
|
||||
this.regexIndex = 0;
|
||||
}
|
||||
|
||||
addRule(re, opts) {
|
||||
this.rules.push([re, opts]);
|
||||
if (opts.type === "begin") this.count++;
|
||||
}
|
||||
|
||||
exec(s) {
|
||||
const m = this.getMatcher(this.regexIndex);
|
||||
m.lastIndex = this.lastIndex;
|
||||
const result = m.exec(s);
|
||||
if (result) {
|
||||
this.regexIndex += result.position + 1;
|
||||
if (this.regexIndex === this.count) { // wrap-around
|
||||
this.regexIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// this.regexIndex = 0;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function buildModeRegex(mode) {
|
||||
const mm = new ResumableMultiRegex();
|
||||
|
||||
mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" }));
|
||||
|
||||
if (mode.terminator_end) {
|
||||
mm.addRule(mode.terminator_end, { type: "end" });
|
||||
}
|
||||
if (mode.illegal) {
|
||||
mm.addRule(mode.illegal, { type: "illegal" });
|
||||
}
|
||||
|
||||
return mm;
|
||||
}
|
||||
|
||||
// TODO: We need negative look-behind support to do this properly
|
||||
function skipIfhasPrecedingOrTrailingDot(match, resp) {
|
||||
const before = match.input[match.index - 1];
|
||||
const after = match.input[match.index + match[0].length];
|
||||
if (before === "." || after === ".") {
|
||||
resp.ignoreMatch();
|
||||
}
|
||||
}
|
||||
|
||||
/** skip vs abort vs ignore
|
||||
*
|
||||
* @skip - The mode is still entered and exited normally (and contains rules apply),
|
||||
* but all content is held and added to the parent buffer rather than being
|
||||
* output when the mode ends. Mostly used with `sublanguage` to build up
|
||||
* a single large buffer than can be parsed by sublanguage.
|
||||
*
|
||||
* - The mode begin ands ends normally.
|
||||
* - Content matched is added to the parent mode buffer.
|
||||
* - The parser cursor is moved forward normally.
|
||||
*
|
||||
* @abort - A hack placeholder until we have ignore. Aborts the mode (as if it
|
||||
* never matched) but DOES NOT continue to match subsequent `contains`
|
||||
* modes. Abort is bad/suboptimal because it can result in modes
|
||||
* farther down not getting applied because an earlier rule eats the
|
||||
* content but then aborts.
|
||||
*
|
||||
* - The mode does not begin.
|
||||
* - Content matched by `begin` is added to the mode buffer.
|
||||
* - The parser cursor is moved forward accordingly.
|
||||
*
|
||||
* @ignore - Ignores the mode (as if it never matched) and continues to match any
|
||||
* subsequent `contains` modes. Ignore isn't technically possible with
|
||||
* the current parser implementation.
|
||||
*
|
||||
* - The mode does not begin.
|
||||
* - Content matched by `begin` is ignored.
|
||||
* - The parser cursor is not moved forward.
|
||||
*/
|
||||
|
||||
function compileMode(mode, parent) {
|
||||
if (mode.compiled) return;
|
||||
mode.compiled = true;
|
||||
|
||||
// __beforeBegin is considered private API, internal use only
|
||||
mode.__beforeBegin = null;
|
||||
|
||||
mode.keywords = mode.keywords || mode.beginKeywords;
|
||||
|
||||
let kw_pattern = null;
|
||||
if (typeof mode.keywords === "object") {
|
||||
kw_pattern = mode.keywords.$pattern;
|
||||
delete mode.keywords.$pattern;
|
||||
}
|
||||
|
||||
if (mode.keywords) {
|
||||
mode.keywords = compileKeywords(mode.keywords, language.case_insensitive);
|
||||
}
|
||||
|
||||
// both are not allowed
|
||||
if (mode.lexemes && kw_pattern) {
|
||||
throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");
|
||||
}
|
||||
|
||||
// `mode.lexemes` was the old standard before we added and now recommend
|
||||
// using `keywords.$pattern` to pass the keyword pattern
|
||||
mode.keywordPatternRe = langRe(mode.lexemes || kw_pattern || /\w+/, true);
|
||||
|
||||
if (parent) {
|
||||
if (mode.beginKeywords) {
|
||||
// for languages with keywords that include non-word characters checking for
|
||||
// a word boundary is not sufficient, so instead we check for a word boundary
|
||||
// or whitespace - this does no harm in any case since our keyword engine
|
||||
// doesn't allow spaces in keywords anyways and we still check for the boundary
|
||||
// first
|
||||
mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?=\\b|\\s)';
|
||||
mode.__beforeBegin = skipIfhasPrecedingOrTrailingDot;
|
||||
}
|
||||
if (!mode.begin)
|
||||
mode.begin = /\B|\b/;
|
||||
mode.beginRe = langRe(mode.begin);
|
||||
if (mode.endSameAsBegin)
|
||||
mode.end = mode.begin;
|
||||
if (!mode.end && !mode.endsWithParent)
|
||||
mode.end = /\B|\b/;
|
||||
if (mode.end)
|
||||
mode.endRe = langRe(mode.end);
|
||||
mode.terminator_end = regex.source(mode.end) || '';
|
||||
if (mode.endsWithParent && parent.terminator_end)
|
||||
mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;
|
||||
}
|
||||
if (mode.illegal)
|
||||
mode.illegalRe = langRe(mode.illegal);
|
||||
if (mode.relevance == null)
|
||||
mode.relevance = 1;
|
||||
if (!mode.contains) {
|
||||
mode.contains = [];
|
||||
}
|
||||
mode.contains = [].concat(...mode.contains.map(function(c) {
|
||||
return expand_or_clone_mode(c === 'self' ? mode : c);
|
||||
}));
|
||||
mode.contains.forEach(function(c) { compileMode(c, mode); });
|
||||
|
||||
if (mode.starts) {
|
||||
compileMode(mode.starts, parent);
|
||||
}
|
||||
|
||||
mode.matcher = buildModeRegex(mode);
|
||||
}
|
||||
|
||||
// self is not valid at the top-level
|
||||
if (language.contains && language.contains.includes('self')) {
|
||||
throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");
|
||||
}
|
||||
compileMode(language);
|
||||
}
|
||||
|
||||
function dependencyOnParent(mode) {
|
||||
if (!mode) return false;
|
||||
|
||||
return mode.endsWithParent || dependencyOnParent(mode.starts);
|
||||
}
|
||||
|
||||
function expand_or_clone_mode(mode) {
|
||||
if (mode.variants && !mode.cached_variants) {
|
||||
mode.cached_variants = mode.variants.map(function(variant) {
|
||||
return inherit(mode, { variants: null }, variant);
|
||||
});
|
||||
}
|
||||
|
||||
// EXPAND
|
||||
// if we have variants then essentially "replace" the mode with the variants
|
||||
// this happens in compileMode, where this function is called from
|
||||
if (mode.cached_variants) {
|
||||
return mode.cached_variants;
|
||||
}
|
||||
|
||||
// CLONE
|
||||
// if we have dependencies on parents then we need a unique
|
||||
// instance of ourselves, so we can be reused with many
|
||||
// different parents without issue
|
||||
if (dependencyOnParent(mode)) {
|
||||
return inherit(mode, { starts: mode.starts ? inherit(mode.starts) : null });
|
||||
}
|
||||
|
||||
if (Object.isFrozen(mode)) {
|
||||
return inherit(mode);
|
||||
}
|
||||
|
||||
// no special dependency issues, just return ourselves
|
||||
return mode;
|
||||
}
|
||||
|
||||
// keywords
|
||||
|
||||
function compileKeywords(rawKeywords, case_insensitive) {
|
||||
var compiled_keywords = {};
|
||||
|
||||
if (typeof rawKeywords === 'string') { // string
|
||||
splitAndCompile('keyword', rawKeywords);
|
||||
} else {
|
||||
Object.keys(rawKeywords).forEach(function(className) {
|
||||
splitAndCompile(className, rawKeywords[className]);
|
||||
});
|
||||
}
|
||||
return compiled_keywords;
|
||||
|
||||
// ---
|
||||
|
||||
function splitAndCompile(className, str) {
|
||||
if (case_insensitive) {
|
||||
str = str.toLowerCase();
|
||||
}
|
||||
str.split(' ').forEach(function(keyword) {
|
||||
var pair = keyword.split('|');
|
||||
compiled_keywords[pair[0]] = [className, scoreForKeyword(pair[0], pair[1])];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scoreForKeyword(keyword, providedScore) {
|
||||
// manual scores always win over common keywords
|
||||
// so you can force a score of 1 if you really insist
|
||||
if (providedScore) {
|
||||
return Number(providedScore);
|
||||
}
|
||||
|
||||
return commonKeyword(keyword) ? 0 : 1;
|
||||
}
|
||||
|
||||
function commonKeyword(word) {
|
||||
return COMMON_KEYWORDS.includes(word.toLowerCase());
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import { inherit } from './utils';
|
||||
import * as regex from './regex';
|
||||
|
||||
// Common regexps
|
||||
export const IDENT_RE = '[a-zA-Z]\\w*';
|
||||
export const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*';
|
||||
export const NUMBER_RE = '\\b\\d+(\\.\\d+)?';
|
||||
export const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float
|
||||
export const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
|
||||
export const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
|
||||
|
||||
export const SHEBANG = (opts = {}) => {
|
||||
const beginShebang = /^#![ ]*\//;
|
||||
if (opts.binary) {
|
||||
opts.begin = regex.concat(
|
||||
beginShebang,
|
||||
/.*\b/,
|
||||
opts.binary,
|
||||
/\b.*/);
|
||||
}
|
||||
return inherit({
|
||||
className: 'meta',
|
||||
begin: beginShebang,
|
||||
end: /$/,
|
||||
relevance: 0,
|
||||
"on:begin": (m, resp) => {
|
||||
if (m.index !== 0) resp.ignoreMatch();
|
||||
}
|
||||
}, opts);
|
||||
};
|
||||
|
||||
// Common modes
|
||||
export const BACKSLASH_ESCAPE = {
|
||||
begin: '\\\\[\\s\\S]', relevance: 0
|
||||
};
|
||||
export const APOS_STRING_MODE = {
|
||||
className: 'string',
|
||||
begin: '\'',
|
||||
end: '\'',
|
||||
illegal: '\\n',
|
||||
contains: [BACKSLASH_ESCAPE]
|
||||
};
|
||||
export const QUOTE_STRING_MODE = {
|
||||
className: 'string',
|
||||
begin: '"',
|
||||
end: '"',
|
||||
illegal: '\\n',
|
||||
contains: [BACKSLASH_ESCAPE]
|
||||
};
|
||||
export const PHRASAL_WORDS_MODE = {
|
||||
begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
|
||||
};
|
||||
export const COMMENT = function(begin, end, inherits) {
|
||||
var mode = inherit(
|
||||
{
|
||||
className: 'comment',
|
||||
begin: begin,
|
||||
end: end,
|
||||
contains: []
|
||||
},
|
||||
inherits || {}
|
||||
);
|
||||
mode.contains.push(PHRASAL_WORDS_MODE);
|
||||
mode.contains.push({
|
||||
className: 'doctag',
|
||||
begin: '(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):',
|
||||
relevance: 0
|
||||
});
|
||||
return mode;
|
||||
};
|
||||
export const C_LINE_COMMENT_MODE = COMMENT('//', '$');
|
||||
export const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/');
|
||||
export const HASH_COMMENT_MODE = COMMENT('#', '$');
|
||||
export const NUMBER_MODE = {
|
||||
className: 'number',
|
||||
begin: NUMBER_RE,
|
||||
relevance: 0
|
||||
};
|
||||
export const C_NUMBER_MODE = {
|
||||
className: 'number',
|
||||
begin: C_NUMBER_RE,
|
||||
relevance: 0
|
||||
};
|
||||
export const BINARY_NUMBER_MODE = {
|
||||
className: 'number',
|
||||
begin: BINARY_NUMBER_RE,
|
||||
relevance: 0
|
||||
};
|
||||
export const CSS_NUMBER_MODE = {
|
||||
className: 'number',
|
||||
begin: NUMBER_RE + '(' +
|
||||
'%|em|ex|ch|rem' +
|
||||
'|vw|vh|vmin|vmax' +
|
||||
'|cm|mm|in|pt|pc|px' +
|
||||
'|deg|grad|rad|turn' +
|
||||
'|s|ms' +
|
||||
'|Hz|kHz' +
|
||||
'|dpi|dpcm|dppx' +
|
||||
')?',
|
||||
relevance: 0
|
||||
};
|
||||
export const REGEXP_MODE = {
|
||||
// this outer rule makes sure we actually have a WHOLE regex and not simply
|
||||
// an expression such as:
|
||||
//
|
||||
// 3 / something
|
||||
//
|
||||
// (which will then blow up when regex's `illegal` sees the newline)
|
||||
begin: /(?=\/[^/\n]*\/)/,
|
||||
contains: [{
|
||||
className: 'regexp',
|
||||
begin: /\//,
|
||||
end: /\/[gimuy]*/,
|
||||
illegal: /\n/,
|
||||
contains: [
|
||||
BACKSLASH_ESCAPE,
|
||||
{
|
||||
begin: /\[/,
|
||||
end: /\]/,
|
||||
relevance: 0,
|
||||
contains: [BACKSLASH_ESCAPE]
|
||||
}
|
||||
]
|
||||
}]
|
||||
};
|
||||
export const TITLE_MODE = {
|
||||
className: 'title',
|
||||
begin: IDENT_RE,
|
||||
relevance: 0
|
||||
};
|
||||
export const UNDERSCORE_TITLE_MODE = {
|
||||
className: 'title',
|
||||
begin: UNDERSCORE_IDENT_RE,
|
||||
relevance: 0
|
||||
};
|
||||
export const METHOD_GUARD = {
|
||||
// excludes method names from keyword processing
|
||||
begin: '\\.\\s*' + UNDERSCORE_IDENT_RE,
|
||||
relevance: 0
|
||||
};
|
||||
|
||||
export const END_SAME_AS_BEGIN = function(mode) {
|
||||
return Object.assign(mode,
|
||||
{
|
||||
'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; },
|
||||
'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch() }
|
||||
});
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
export function escape(value) {
|
||||
return new RegExp(value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm');
|
||||
}
|
||||
|
||||
export function source(re) {
|
||||
// if it's a regex get it's source,
|
||||
// otherwise it's a string already so just return it
|
||||
return (re && re.source) || re;
|
||||
}
|
||||
|
||||
export function lookahead(regex) {
|
||||
return concat('(?=', regex, ')');
|
||||
}
|
||||
|
||||
export function concat(...args) {
|
||||
const joined = args.map((x) => source(x)).join("");
|
||||
return joined;
|
||||
}
|
||||
|
||||
export function countMatchGroups(re) {
|
||||
return (new RegExp(re.toString() + '|')).exec('').length - 1;
|
||||
}
|
||||
|
||||
export function startsWith(re, lexeme) {
|
||||
var match = re && re.exec(lexeme);
|
||||
return match && match.index === 0;
|
||||
}
|
||||
|
||||
// join logically computes regexps.join(separator), but fixes the
|
||||
// backreferences so they continue to match.
|
||||
// it also places each individual regular expression into it's own
|
||||
// match group, keeping track of the sequencing of those match groups
|
||||
// is currently an exercise for the caller. :-)
|
||||
export function join(regexps, separator) {
|
||||
// backreferenceRe matches an open parenthesis or backreference. To avoid
|
||||
// an incorrect parse, it additionally matches the following:
|
||||
// - [...] elements, where the meaning of parentheses and escapes change
|
||||
// - other escape sequences, so we do not misparse escape sequences as
|
||||
// interesting elements
|
||||
// - non-matching or lookahead parentheses, which do not capture. These
|
||||
// follow the '(' with a '?'.
|
||||
var backreferenceRe = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
|
||||
var numCaptures = 0;
|
||||
var ret = '';
|
||||
for (var i = 0; i < regexps.length; i++) {
|
||||
numCaptures += 1;
|
||||
var offset = numCaptures;
|
||||
var re = source(regexps[i]);
|
||||
if (i > 0) {
|
||||
ret += separator;
|
||||
}
|
||||
ret += "(";
|
||||
while (re.length > 0) {
|
||||
var match = backreferenceRe.exec(re);
|
||||
if (match == null) {
|
||||
ret += re;
|
||||
break;
|
||||
}
|
||||
ret += re.substring(0, match.index);
|
||||
re = re.substring(match.index + match[0].length);
|
||||
if (match[0][0] === '\\' && match[1]) {
|
||||
// Adjust the backreference.
|
||||
ret += '\\' + String(Number(match[1]) + offset);
|
||||
} else {
|
||||
ret += match[0];
|
||||
if (match[0] === '(') {
|
||||
numCaptures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
ret += ")";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export default class Response {
|
||||
constructor(mode) {
|
||||
if (mode.data === undefined)
|
||||
mode.data = {};
|
||||
this.data = mode.data;
|
||||
}
|
||||
|
||||
ignoreMatch() {
|
||||
this.ignore = true;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import HTMLRenderer from './html_renderer';
|
||||
|
||||
class TokenTree {
|
||||
constructor() {
|
||||
this.rootNode = { children: [] };
|
||||
this.stack = [this.rootNode];
|
||||
}
|
||||
|
||||
get top() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
get root() { return this.rootNode; }
|
||||
|
||||
add(node) {
|
||||
this.top.children.push(node);
|
||||
}
|
||||
|
||||
openNode(kind) {
|
||||
const node = { kind, children: [] };
|
||||
this.add(node);
|
||||
this.stack.push(node);
|
||||
}
|
||||
|
||||
closeNode() {
|
||||
if (this.stack.length > 1) {
|
||||
return this.stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
closeAllNodes() {
|
||||
while (this.closeNode());
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return JSON.stringify(this.rootNode, null, 4);
|
||||
}
|
||||
|
||||
walk(builder) {
|
||||
return this.constructor._walk(builder, this.rootNode);
|
||||
}
|
||||
|
||||
static _walk(builder, node) {
|
||||
if (typeof node === "string") {
|
||||
builder.addText(node);
|
||||
} else if (node.children) {
|
||||
builder.openNode(node);
|
||||
node.children.forEach((child) => this._walk(builder, child));
|
||||
builder.closeNode(node);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
static _collapse(node) {
|
||||
if (!node.children) {
|
||||
return;
|
||||
}
|
||||
if (node.children.every(el => typeof el === "string")) {
|
||||
node.text = node.children.join("");
|
||||
delete node.children;
|
||||
} else {
|
||||
node.children.forEach((child) => {
|
||||
if (typeof child === "string") return;
|
||||
TokenTree._collapse(child);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Currently this is all private API, but this is the minimal API necessary
|
||||
that an Emitter must implement to fully support the parser.
|
||||
|
||||
Minimal interface:
|
||||
|
||||
- addKeyword(text, kind)
|
||||
- addText(text)
|
||||
- addSublanguage(emitter, subLanguageName)
|
||||
- finalize()
|
||||
- openNode(kind)
|
||||
- closeNode()
|
||||
- closeAllNodes()
|
||||
- toHTML()
|
||||
|
||||
*/
|
||||
export default class TokenTreeEmitter extends TokenTree {
|
||||
constructor(options) {
|
||||
super();
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
addKeyword(text, kind) {
|
||||
if (text === "") { return; }
|
||||
|
||||
this.openNode(kind);
|
||||
this.addText(text);
|
||||
this.closeNode();
|
||||
}
|
||||
|
||||
addText(text) {
|
||||
if (text === "") { return; }
|
||||
|
||||
this.add(text);
|
||||
}
|
||||
|
||||
addSublanguage(emitter, name) {
|
||||
const node = emitter.root;
|
||||
node.kind = name;
|
||||
node.sublanguage = true;
|
||||
this.add(node);
|
||||
}
|
||||
|
||||
toHTML() {
|
||||
const renderer = new HTMLRenderer(this, this.options);
|
||||
return renderer.value();
|
||||
}
|
||||
|
||||
finalize() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
export function escapeHTML(value) {
|
||||
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* performs a shallow merge of multiple objects into one
|
||||
*
|
||||
* @arguments list of objects with properties to merge
|
||||
* @returns a single new object
|
||||
*/
|
||||
export function inherit(parent) { // inherit(parent, override_obj, override_obj, ...)
|
||||
var result = {};
|
||||
var objects = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
for (const key in parent) {
|
||||
result[key] = parent[key];
|
||||
}
|
||||
objects.forEach(function(obj) {
|
||||
for (const key in obj) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Stream merging */
|
||||
|
||||
function tag(node) {
|
||||
return node.nodeName.toLowerCase();
|
||||
}
|
||||
|
||||
export function nodeStream(node) {
|
||||
var result = [];
|
||||
(function _nodeStream(node, offset) {
|
||||
for (var child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (child.nodeType === 3) {
|
||||
offset += child.nodeValue.length;
|
||||
} else if (child.nodeType === 1) {
|
||||
result.push({
|
||||
event: 'start',
|
||||
offset: offset,
|
||||
node: child
|
||||
});
|
||||
offset = _nodeStream(child, offset);
|
||||
// Prevent void elements from having an end tag that would actually
|
||||
// double them in the output. There are more void elements in HTML
|
||||
// but we list only those realistically expected in code display.
|
||||
if (!tag(child).match(/br|hr|img|input/)) {
|
||||
result.push({
|
||||
event: 'stop',
|
||||
offset: offset,
|
||||
node: child
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
})(node, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function mergeStreams(original, highlighted, value) {
|
||||
var processed = 0;
|
||||
var result = '';
|
||||
var nodeStack = [];
|
||||
|
||||
function selectStream() {
|
||||
if (!original.length || !highlighted.length) {
|
||||
return original.length ? original : highlighted;
|
||||
}
|
||||
if (original[0].offset !== highlighted[0].offset) {
|
||||
return (original[0].offset < highlighted[0].offset) ? original : highlighted;
|
||||
}
|
||||
|
||||
/*
|
||||
To avoid starting the stream just before it should stop the order is
|
||||
ensured that original always starts first and closes last:
|
||||
|
||||
if (event1 == 'start' && event2 == 'start')
|
||||
return original;
|
||||
if (event1 == 'start' && event2 == 'stop')
|
||||
return highlighted;
|
||||
if (event1 == 'stop' && event2 == 'start')
|
||||
return original;
|
||||
if (event1 == 'stop' && event2 == 'stop')
|
||||
return highlighted;
|
||||
|
||||
... which is collapsed to:
|
||||
*/
|
||||
return highlighted[0].event === 'start' ? original : highlighted;
|
||||
}
|
||||
|
||||
function open(node) {
|
||||
function attr_str(a) {
|
||||
return ' ' + a.nodeName + '="' + escapeHTML(a.value).replace(/"/g, '"') + '"';
|
||||
}
|
||||
result += '<' + tag(node) + [].map.call(node.attributes, attr_str).join('') + '>';
|
||||
}
|
||||
|
||||
function close(node) {
|
||||
result += '</' + tag(node) + '>';
|
||||
}
|
||||
|
||||
function render(event) {
|
||||
(event.event === 'start' ? open : close)(event.node);
|
||||
}
|
||||
|
||||
while (original.length || highlighted.length) {
|
||||
var stream = selectStream();
|
||||
result += escapeHTML(value.substring(processed, stream[0].offset));
|
||||
processed = stream[0].offset;
|
||||
if (stream === original) {
|
||||
/*
|
||||
On any opening or closing tag of the original markup we first close
|
||||
the entire highlighted node stack, then render the original tag along
|
||||
with all the following original tags at the same offset and then
|
||||
reopen all the tags on the highlighted stack.
|
||||
*/
|
||||
nodeStack.reverse().forEach(close);
|
||||
do {
|
||||
render(stream.splice(0, 1)[0]);
|
||||
stream = selectStream();
|
||||
} while (stream === original && stream.length && stream[0].offset === processed);
|
||||
nodeStack.reverse().forEach(open);
|
||||
} else {
|
||||
if (stream[0].event === 'start') {
|
||||
nodeStack.push(stream[0].node);
|
||||
} else {
|
||||
nodeStack.pop();
|
||||
}
|
||||
render(stream.splice(0, 1)[0]);
|
||||
}
|
||||
}
|
||||
return result + escapeHTML(value.substr(processed));
|
||||
}
|
||||
Reference in New Issue
Block a user