summaryrefslogtreecommitdiff
path: root/packages/astro-parser/src/utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro-parser/src/utils')
-rw-r--r--packages/astro-parser/src/utils/error.ts46
-rw-r--r--packages/astro-parser/src/utils/full_char_code_at.ts11
-rw-r--r--packages/astro-parser/src/utils/fuzzymatch.ts233
-rw-r--r--packages/astro-parser/src/utils/get_code_frame.ts29
-rw-r--r--packages/astro-parser/src/utils/link.ts5
-rw-r--r--packages/astro-parser/src/utils/list.ts5
-rw-r--r--packages/astro-parser/src/utils/names.ts142
-rw-r--r--packages/astro-parser/src/utils/namespaces.ts13
-rw-r--r--packages/astro-parser/src/utils/nodes_match.ts35
-rw-r--r--packages/astro-parser/src/utils/patterns.ts3
-rw-r--r--packages/astro-parser/src/utils/trim.ts17
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);
+}