diff options
Diffstat (limited to 'packages/astro-parser/src/utils')
-rw-r--r-- | packages/astro-parser/src/utils/error.ts | 46 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/full_char_code_at.ts | 11 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/fuzzymatch.ts | 233 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/get_code_frame.ts | 29 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/link.ts | 5 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/list.ts | 5 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/names.ts | 142 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/namespaces.ts | 13 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/nodes_match.ts | 35 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/patterns.ts | 3 | ||||
-rw-r--r-- | packages/astro-parser/src/utils/trim.ts | 17 |
11 files changed, 539 insertions, 0 deletions
diff --git a/packages/astro-parser/src/utils/error.ts b/packages/astro-parser/src/utils/error.ts new file mode 100644 index 000000000..8ebb5b093 --- /dev/null +++ b/packages/astro-parser/src/utils/error.ts @@ -0,0 +1,46 @@ +// @ts-nocheck + +import { locate } from 'locate-character'; +import get_code_frame from './get_code_frame.js'; + +export class CompileError extends Error { + code: string; + start: { line: number; column: number }; + end: { line: number; column: number }; + pos: number; + filename: string; + frame: string; + + toString() { + return `${this.message} (${this.start.line}:${this.start.column})\n${this.frame}`; + } +} + +/** Throw CompileError */ +export default function error( + message: string, + props: { + name: string; + code: string; + source: string; + filename: string; + start: number; + end?: number; + } +): never { + const err = new CompileError(message); + err.name = props.name; + + const start = locate(props.source, props.start, { offsetLine: 1 }); + const end = locate(props.source, props.end || props.start, { offsetLine: 1 }); + + err.code = props.code; + err.start = start; + err.end = end; + err.pos = props.start; + err.filename = props.filename; + + err.frame = get_code_frame(props.source, start.line - 1, start.column); + + throw err; +} diff --git a/packages/astro-parser/src/utils/full_char_code_at.ts b/packages/astro-parser/src/utils/full_char_code_at.ts new file mode 100644 index 000000000..b62b2c77a --- /dev/null +++ b/packages/astro-parser/src/utils/full_char_code_at.ts @@ -0,0 +1,11 @@ +// Adapted from https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js +// Reproduced under MIT License https://github.com/acornjs/acorn/blob/master/LICENSE + +/** @url https://github.com/acornjs/acorn/blob/6584815dca7440e00de841d1dad152302fdd7ca5/src/tokenize.js */ +export default function full_char_code_at(str: string, i: number): number { + const code = str.charCodeAt(i); + if (code <= 0xd7ff || code >= 0xe000) return code; + + const next = str.charCodeAt(i + 1); + return (code << 10) + next - 0x35fdc00; +} diff --git a/packages/astro-parser/src/utils/fuzzymatch.ts b/packages/astro-parser/src/utils/fuzzymatch.ts new file mode 100644 index 000000000..4d17aafdf --- /dev/null +++ b/packages/astro-parser/src/utils/fuzzymatch.ts @@ -0,0 +1,233 @@ +// @ts-nocheck + +/** Utility for accessing FuzzySet */ +export default function fuzzymatch(name: string, names: string[]) { + const set = new FuzzySet(names); + const matches = set.get(name); + + return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null; +} + +// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js +// BSD Licensed + +const GRAM_SIZE_LOWER = 2; +const GRAM_SIZE_UPPER = 3; + +/** Return an edit distance from 0 to 1 */ +function _distance(str1: string, str2: string) { + if (str1 === null && str2 === null) { + throw 'Trying to compare two null values'; + } + if (str1 === null || str2 === null) return 0; + str1 = String(str1); + str2 = String(str2); + + const distance = levenshtein(str1, str2); + if (str1.length > str2.length) { + return 1 - distance / str1.length; + } else { + return 1 - distance / str2.length; + } +} + +/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L18 */ +function levenshtein(str1: string, str2: string) { + const current: number[] = []; + let prev; + let value; + + for (let i = 0; i <= str2.length; i++) { + for (let j = 0; j <= str1.length; j++) { + if (i && j) { + if (str1.charAt(j - 1) === str2.charAt(i - 1)) { + value = prev; + } else { + value = Math.min(current[j], current[j - 1], prev) + 1; + } + } else { + value = i + j; + } + + prev = current[j]; + current[j] = value; + } + } + + return current.pop(); +} + +const non_word_regex = /[^\w, ]+/; + +/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L53 */ +function iterate_grams(value: string, gram_size = 2) { + const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-'; + const len_diff = gram_size - simplified.length; + const results = []; + + if (len_diff > 0) { + for (let i = 0; i < len_diff; ++i) { + value += '-'; + } + } + for (let i = 0; i < simplified.length - gram_size + 1; ++i) { + results.push(simplified.slice(i, i + gram_size)); + } + return results; +} + +/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L69 */ +function gram_counter(value: string, gram_size = 2) { + // return an object where key=gram, value=number of occurrences + const result = {}; + const grams = iterate_grams(value, gram_size); + let i = 0; + + for (i; i < grams.length; ++i) { + if (grams[i] in result) { + result[grams[i]] += 1; + } else { + result[grams[i]] = 1; + } + } + return result; +} + +/** @url https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js#L158 */ +function sort_descending(a, b) { + return b[0] - a[0]; +} + +class FuzzySet { + exact_set = {}; + match_dict = {}; + items = {}; + + constructor(arr: string[]) { + // initialization + for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) { + this.items[i] = []; + } + + // add all the items to the set + for (let i = 0; i < arr.length; ++i) { + this.add(arr[i]); + } + } + + add(value: string) { + const normalized_value = value.toLowerCase(); + if (normalized_value in this.exact_set) { + return false; + } + + let i = GRAM_SIZE_LOWER; + for (i; i < GRAM_SIZE_UPPER + 1; ++i) { + this._add(value, i); + } + } + + _add(value: string, gram_size: number) { + const normalized_value = value.toLowerCase(); + const items = this.items[gram_size] || []; + const index = items.length; + + items.push(0); + const gram_counts = gram_counter(normalized_value, gram_size); + let sum_of_square_gram_counts = 0; + let gram; + let gram_count; + + for (gram in gram_counts) { + gram_count = gram_counts[gram]; + sum_of_square_gram_counts += Math.pow(gram_count, 2); + if (gram in this.match_dict) { + this.match_dict[gram].push([index, gram_count]); + } else { + this.match_dict[gram] = [[index, gram_count]]; + } + } + const vector_normal = Math.sqrt(sum_of_square_gram_counts); + items[index] = [vector_normal, normalized_value]; + this.items[gram_size] = items; + this.exact_set[normalized_value] = value; + } + + get(value: string) { + const normalized_value = value.toLowerCase(); + const result = this.exact_set[normalized_value]; + + if (result) { + return [[1, result]]; + } + + let results = []; + // start with high gram size and if there are no results, go to lower gram sizes + for (let gram_size = GRAM_SIZE_UPPER; gram_size >= GRAM_SIZE_LOWER; --gram_size) { + results = this.__get(value, gram_size); + if (results) { + return results; + } + } + return null; + } + + __get(value: string, gram_size: number) { + const normalized_value = value.toLowerCase(); + const matches = {}; + const gram_counts = gram_counter(normalized_value, gram_size); + const items = this.items[gram_size]; + let sum_of_square_gram_counts = 0; + let gram; + let gram_count; + let i; + let index; + let other_gram_count; + + for (gram in gram_counts) { + gram_count = gram_counts[gram]; + sum_of_square_gram_counts += Math.pow(gram_count, 2); + if (gram in this.match_dict) { + for (i = 0; i < this.match_dict[gram].length; ++i) { + index = this.match_dict[gram][i][0]; + other_gram_count = this.match_dict[gram][i][1]; + if (index in matches) { + matches[index] += gram_count * other_gram_count; + } else { + matches[index] = gram_count * other_gram_count; + } + } + } + } + + const vector_normal = Math.sqrt(sum_of_square_gram_counts); + let results = []; + let match_score; + + // build a results list of [score, str] + for (const match_index in matches) { + match_score = matches[match_index]; + results.push([match_score / (vector_normal * items[match_index][0]), items[match_index][1]]); + } + + results.sort(sort_descending); + + let new_results = []; + const end_index = Math.min(50, results.length); + // truncate somewhat arbitrarily to 50 + for (let j = 0; j < end_index; ++j) { + new_results.push([_distance(results[j][1], normalized_value), results[j][1]]); + } + results = new_results; + results.sort(sort_descending); + + new_results = []; + for (let j = 0; j < results.length; ++j) { + if (results[j][0] == results[0][0]) { + new_results.push([results[j][0], this.exact_set[results[j][1]]]); + } + } + + return new_results; + } +} diff --git a/packages/astro-parser/src/utils/get_code_frame.ts b/packages/astro-parser/src/utils/get_code_frame.ts new file mode 100644 index 000000000..e4f1834fd --- /dev/null +++ b/packages/astro-parser/src/utils/get_code_frame.ts @@ -0,0 +1,29 @@ +/** Die you stupid tabs */ +function tabs_to_spaces(str: string) { + return str.replace(/^\t+/, (match) => match.split('\t').join(' ')); +} + +/** Display syntax error in pretty format in logs */ +export default function get_code_frame(source: string, line: number, column: number) { + const lines = source.split('\n'); + + const frame_start = Math.max(0, line - 2); + const frame_end = Math.min(line + 3, lines.length); + + const digits = String(frame_end + 1).length; + + return lines + .slice(frame_start, frame_end) + .map((str, i) => { + const isErrorLine = frame_start + i === line; + const line_num = String(i + frame_start + 1).padStart(digits, ' '); + + if (isErrorLine) { + const indicator = ' '.repeat(digits + 2 + tabs_to_spaces(str.slice(0, column)).length) + '^'; + return `${line_num}: ${tabs_to_spaces(str)}\n${indicator}`; + } + + return `${line_num}: ${tabs_to_spaces(str)}`; + }) + .join('\n'); +} diff --git a/packages/astro-parser/src/utils/link.ts b/packages/astro-parser/src/utils/link.ts new file mode 100644 index 000000000..4e2ed662f --- /dev/null +++ b/packages/astro-parser/src/utils/link.ts @@ -0,0 +1,5 @@ +/** Linked list */ +export function link<T extends { next?: T; prev?: T }>(next: T, prev: T) { + prev.next = next; + if (next) next.prev = prev; +} diff --git a/packages/astro-parser/src/utils/list.ts b/packages/astro-parser/src/utils/list.ts new file mode 100644 index 000000000..9388adb14 --- /dev/null +++ b/packages/astro-parser/src/utils/list.ts @@ -0,0 +1,5 @@ +/** Display an array of strings in a human-readable format */ +export default function list(items: string[], conjunction = 'or') { + if (items.length === 1) return items[0]; + return `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`; +} diff --git a/packages/astro-parser/src/utils/names.ts b/packages/astro-parser/src/utils/names.ts new file mode 100644 index 000000000..f041d20ce --- /dev/null +++ b/packages/astro-parser/src/utils/names.ts @@ -0,0 +1,142 @@ +import { isIdentifierStart, isIdentifierChar } from 'acorn'; +import full_char_code_at from './full_char_code_at.js'; + +export const globals = new Set([ + 'alert', + 'Array', + 'Boolean', + 'clearInterval', + 'clearTimeout', + 'confirm', + 'console', + 'Date', + 'decodeURI', + 'decodeURIComponent', + 'document', + 'Element', + 'encodeURI', + 'encodeURIComponent', + 'Error', + 'EvalError', + 'Event', + 'EventSource', + 'fetch', + 'global', + 'globalThis', + 'history', + 'Infinity', + 'InternalError', + 'Intl', + 'isFinite', + 'isNaN', + 'JSON', + 'localStorage', + 'location', + 'Map', + 'Math', + 'NaN', + 'navigator', + 'Number', + 'Node', + 'Object', + 'parseFloat', + 'parseInt', + 'process', + 'Promise', + 'prompt', + 'RangeError', + 'ReferenceError', + 'RegExp', + 'sessionStorage', + 'Set', + 'setInterval', + 'setTimeout', + 'String', + 'SyntaxError', + 'TypeError', + 'undefined', + 'URIError', + 'URL', + 'window', +]); + +export const reserved = new Set([ + 'arguments', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'enum', + 'eval', + 'export', + 'extends', + 'false', + 'finally', + 'for', + 'function', + 'if', + 'implements', + 'import', + 'in', + 'instanceof', + 'interface', + 'let', + 'new', + 'null', + 'package', + 'private', + 'protected', + 'public', + 'return', + 'static', + 'super', + 'switch', + 'this', + 'throw', + 'true', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', +]); + +const void_element_names = /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; + +/** Is this a void HTML element? */ +export function is_void(name: string) { + return void_element_names.test(name) || name.toLowerCase() === '!doctype'; +} + +/** Is this a valid HTML element? */ +export function is_valid(str: string): boolean { + let i = 0; + + while (i < str.length) { + const code = full_char_code_at(str, i); + if (!(i === 0 ? isIdentifierStart : isIdentifierChar)(code, true)) return false; + + i += code <= 0xffff ? 1 : 2; + } + + return true; +} + +/** Utility to normalize HTML */ +export function sanitize(name: string) { + return name + .replace(/[^a-zA-Z0-9_]+/g, '_') + .replace(/^_/, '') + .replace(/_$/, '') + .replace(/^[0-9]/, '_$&'); +} diff --git a/packages/astro-parser/src/utils/namespaces.ts b/packages/astro-parser/src/utils/namespaces.ts new file mode 100644 index 000000000..5f61beff9 --- /dev/null +++ b/packages/astro-parser/src/utils/namespaces.ts @@ -0,0 +1,13 @@ +// The `foreign` namespace covers all DOM implementations that aren't HTML5. +// It opts out of HTML5-specific a11y checks and case-insensitive attribute names. +export const foreign = 'https://svelte.dev/docs#svelte_options'; +export const html = 'http://www.w3.org/1999/xhtml'; +export const mathml = 'http://www.w3.org/1998/Math/MathML'; +export const svg = 'http://www.w3.org/2000/svg'; +export const xlink = 'http://www.w3.org/1999/xlink'; +export const xml = 'http://www.w3.org/XML/1998/namespace'; +export const xmlns = 'http://www.w3.org/2000/xmlns'; + +export const valid_namespaces = ['foreign', 'html', 'mathml', 'svg', 'xlink', 'xml', 'xmlns', foreign, html, mathml, svg, xlink, xml, xmlns]; + +export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns }; diff --git a/packages/astro-parser/src/utils/nodes_match.ts b/packages/astro-parser/src/utils/nodes_match.ts new file mode 100644 index 000000000..7e4093994 --- /dev/null +++ b/packages/astro-parser/src/utils/nodes_match.ts @@ -0,0 +1,35 @@ +// @ts-nocheck + +/** Compare two TemplateNodes to determine if they are equivalent */ +export function nodes_match(a, b) { + if (!!a !== !!b) return false; + if (Array.isArray(a) !== Array.isArray(b)) return false; + + if (a && typeof a === 'object') { + if (Array.isArray(a)) { + if (a.length !== b.length) return false; + return a.every((child, i) => nodes_match(child, b[i])); + } + + const a_keys = Object.keys(a).sort(); + const b_keys = Object.keys(b).sort(); + + if (a_keys.length !== b_keys.length) return false; + + let i = a_keys.length; + while (i--) { + const key = a_keys[i]; + if (b_keys[i] !== key) return false; + + if (key === 'start' || key === 'end') continue; + + if (!nodes_match(a[key], b[key])) { + return false; + } + } + + return true; + } + + return a === b; +} diff --git a/packages/astro-parser/src/utils/patterns.ts b/packages/astro-parser/src/utils/patterns.ts new file mode 100644 index 000000000..317a7c199 --- /dev/null +++ b/packages/astro-parser/src/utils/patterns.ts @@ -0,0 +1,3 @@ +export const whitespace = /[ \t\r\n]/; + +export const dimensions = /^(?:offset|client)(?:Width|Height)$/; diff --git a/packages/astro-parser/src/utils/trim.ts b/packages/astro-parser/src/utils/trim.ts new file mode 100644 index 000000000..480cc99a8 --- /dev/null +++ b/packages/astro-parser/src/utils/trim.ts @@ -0,0 +1,17 @@ +import { whitespace } from './patterns.js'; + +/** Trim whitespace from start of string */ +export function trim_start(str: string) { + let i = 0; + while (whitespace.test(str[i])) i += 1; + + return str.slice(i); +} + +/** Trim whitespace from end of string */ +export function trim_end(str: string) { + let i = str.length; + while (whitespace.test(str[i - 1])) i -= 1; + + return str.slice(0, i); +} |