gcyphrq 0.12.6
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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/dist/engine/cypher-engine.d.ts +39 -0
- package/dist/engine/cypher-engine.d.ts.map +1 -0
- package/dist/engine/cypher-parser.d.ts +3 -0
- package/dist/engine/cypher-parser.d.ts.map +1 -0
- package/dist/graph.d.ts +24 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/index.js +5816 -0
- package/dist/indexes.d.ts +38 -0
- package/dist/indexes.d.ts.map +1 -0
- package/dist/lib.d.ts +189 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +1455 -0
- package/dist/types/cypher.d.ts +139 -0
- package/dist/types/cypher.d.ts.map +1 -0
- package/docs/404.md +11 -0
- package/docs/_config.yml +42 -0
- package/docs/_includes/footer.html +6 -0
- package/docs/_includes/head.html +13 -0
- package/docs/_includes/nav.html +23 -0
- package/docs/_layouts/default.html +20 -0
- package/docs/assets/cypher-highlight.js +171 -0
- package/docs/assets/favicon.svg +4 -0
- package/docs/assets/main.css +416 -0
- package/docs/cli.md +115 -0
- package/docs/examples.md +223 -0
- package/docs/getting-started.md +158 -0
- package/docs/index.md +89 -0
- package/docs/library-api.md +618 -0
- package/docs/query-guide.md +302 -0
- package/docs/skill.md +219 -0
- package/examples/README.md +187 -0
- package/examples/cloud-infra.json +199 -0
- package/examples/social-graph.json +11 -0
- package/package.json +71 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type CypherLiteral = string | number | boolean | null;
|
|
2
|
+
export interface CypherNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
[key: string]: CypherLiteral | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface CypherEdge {
|
|
8
|
+
id: string;
|
|
9
|
+
type?: string;
|
|
10
|
+
[key: string]: CypherLiteral | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pre-computed indexes for fast node/edge lookup during query execution.
|
|
14
|
+
* Built once at graph construction time, used by the engine to avoid
|
|
15
|
+
* full-graph scans during MATCH, WHERE, and traversal.
|
|
16
|
+
*
|
|
17
|
+
* Note: property values are coerced to strings via `String(value)`, so
|
|
18
|
+
* `true`/`false` become `"true"`/`"false"` and `0` becomes `"0"`.
|
|
19
|
+
*/
|
|
20
|
+
export interface GraphIndexes {
|
|
21
|
+
/** label → set of node IDs */
|
|
22
|
+
labelIndex: Map<string, Set<string>>;
|
|
23
|
+
/** propertyKey → propertyValue → set of node IDs (values are string-coerced) */
|
|
24
|
+
propertyIndex: Map<string, Map<string, Set<string>>>;
|
|
25
|
+
/**
|
|
26
|
+
* Edge-type adjacency index.
|
|
27
|
+
* out[type][source] = set of { target, edgeId } for outgoing edges
|
|
28
|
+
* in[type][target] = set of { source, edgeId } for incoming edges
|
|
29
|
+
*/
|
|
30
|
+
edgeTypeIndex: {
|
|
31
|
+
out: Map<string, Map<string, Array<{
|
|
32
|
+
target: string;
|
|
33
|
+
edgeId: string;
|
|
34
|
+
}>>>;
|
|
35
|
+
in: Map<string, Map<string, Array<{
|
|
36
|
+
source: string;
|
|
37
|
+
edgeId: string;
|
|
38
|
+
}>>>;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export type CypherValue = CypherNode | CypherEdge[] | CypherLiteral | null | undefined;
|
|
42
|
+
export type Direction = 'OUT' | 'IN' | 'UNDIRECTED';
|
|
43
|
+
export interface NodePattern {
|
|
44
|
+
variable: string;
|
|
45
|
+
label: string | undefined;
|
|
46
|
+
properties: Record<string, CypherLiteral> | undefined;
|
|
47
|
+
}
|
|
48
|
+
export interface RelationPattern {
|
|
49
|
+
variable: string | undefined;
|
|
50
|
+
type: string | undefined;
|
|
51
|
+
minDepth: number | undefined;
|
|
52
|
+
maxDepth: number | undefined;
|
|
53
|
+
direction: Direction;
|
|
54
|
+
}
|
|
55
|
+
export interface MatchClause {
|
|
56
|
+
optional: boolean;
|
|
57
|
+
hasChains: boolean;
|
|
58
|
+
sourcePattern: NodePattern;
|
|
59
|
+
relationPattern: RelationPattern;
|
|
60
|
+
targetPattern: NodePattern;
|
|
61
|
+
}
|
|
62
|
+
export interface CreateClause {
|
|
63
|
+
type: 'CREATE';
|
|
64
|
+
variable: string;
|
|
65
|
+
label: string | undefined;
|
|
66
|
+
properties: Record<string, CypherLiteral> | undefined;
|
|
67
|
+
}
|
|
68
|
+
export interface DeleteClause {
|
|
69
|
+
type: 'DELETE';
|
|
70
|
+
variable: string;
|
|
71
|
+
}
|
|
72
|
+
export interface SetClause {
|
|
73
|
+
type: 'SET';
|
|
74
|
+
variable: string;
|
|
75
|
+
property: string;
|
|
76
|
+
value: CypherLiteral;
|
|
77
|
+
}
|
|
78
|
+
export type WriteClause = CreateClause | DeleteClause | SetClause;
|
|
79
|
+
export interface PropertyAccessExpression {
|
|
80
|
+
type: 'PropertyAccess';
|
|
81
|
+
variable: string;
|
|
82
|
+
property: string | undefined;
|
|
83
|
+
}
|
|
84
|
+
export interface LiteralExpression {
|
|
85
|
+
type: 'Literal';
|
|
86
|
+
value: CypherLiteral;
|
|
87
|
+
}
|
|
88
|
+
export interface AggregationExpression {
|
|
89
|
+
type: 'Aggregation';
|
|
90
|
+
aggregationType: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
|
|
91
|
+
variable: string;
|
|
92
|
+
property: string | undefined;
|
|
93
|
+
}
|
|
94
|
+
export type Expression = PropertyAccessExpression | LiteralExpression | AggregationExpression;
|
|
95
|
+
export interface BinaryExpression {
|
|
96
|
+
type: 'BinaryExpression';
|
|
97
|
+
operator: '>' | '<' | '=' | 'CONTAINS';
|
|
98
|
+
left: Expression;
|
|
99
|
+
right: Expression;
|
|
100
|
+
}
|
|
101
|
+
export interface Projection {
|
|
102
|
+
expression: Expression;
|
|
103
|
+
alias: string;
|
|
104
|
+
}
|
|
105
|
+
export interface WithClause {
|
|
106
|
+
projections: Projection[];
|
|
107
|
+
where: BinaryExpression | undefined;
|
|
108
|
+
orderBy: OrderByItem[] | undefined;
|
|
109
|
+
skip: number | undefined;
|
|
110
|
+
limit: number | undefined;
|
|
111
|
+
}
|
|
112
|
+
export interface OrderByItem {
|
|
113
|
+
expression: Expression;
|
|
114
|
+
direction: 'ASC' | 'DESC';
|
|
115
|
+
}
|
|
116
|
+
export interface ReturnClause {
|
|
117
|
+
projections: Projection[];
|
|
118
|
+
orderBy: OrderByItem[] | undefined;
|
|
119
|
+
skip: number | undefined;
|
|
120
|
+
limit: number | undefined;
|
|
121
|
+
}
|
|
122
|
+
export type Stage = {
|
|
123
|
+
type: 'MATCH';
|
|
124
|
+
clause: MatchClause;
|
|
125
|
+
} | {
|
|
126
|
+
type: 'WITH';
|
|
127
|
+
clause: WithClause;
|
|
128
|
+
} | {
|
|
129
|
+
type: 'WRITE';
|
|
130
|
+
clause: WriteClause;
|
|
131
|
+
};
|
|
132
|
+
export interface AdvancedCypherAST {
|
|
133
|
+
type: 'Query';
|
|
134
|
+
stages: Stage[];
|
|
135
|
+
return: ReturnClause | undefined;
|
|
136
|
+
}
|
|
137
|
+
export type QueryContext = Record<string, CypherNode | CypherEdge[] | CypherLiteral | null | undefined>;
|
|
138
|
+
export type ResultRow = Record<string, CypherNode | CypherEdge[] | CypherLiteral | null | undefined>;
|
|
139
|
+
//# sourceMappingURL=cypher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cypher.d.ts","sourceRoot":"","sources":["../../src/types/cypher.ts"],"names":[],"mappings":"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"}
|
package/docs/404.md
ADDED
package/docs/_config.yml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
title: gcyphrq
|
|
2
|
+
description: A Cypher graph query engine for in-memory graphs built on Graphology. Available as a CLI tool and Node.js library.
|
|
3
|
+
url: "https://plelevier.github.io"
|
|
4
|
+
baseurl: "/gcyphrq"
|
|
5
|
+
repository: "plelevier/gcyphrq"
|
|
6
|
+
|
|
7
|
+
permalink: pretty
|
|
8
|
+
|
|
9
|
+
markdown: kramdown
|
|
10
|
+
kramdown:
|
|
11
|
+
syntax_highlighter: rouge
|
|
12
|
+
syntax_highlighter_opts:
|
|
13
|
+
css_class: 'highlight'
|
|
14
|
+
span:
|
|
15
|
+
line_numbers: false
|
|
16
|
+
block:
|
|
17
|
+
line_numbers: false
|
|
18
|
+
start_line: 1
|
|
19
|
+
|
|
20
|
+
plugins: []
|
|
21
|
+
|
|
22
|
+
# Navigation
|
|
23
|
+
nav:
|
|
24
|
+
- title: Getting Started
|
|
25
|
+
url: /getting-started/
|
|
26
|
+
- title: CLI Reference
|
|
27
|
+
url: /cli/
|
|
28
|
+
- title: Library API
|
|
29
|
+
url: /library-api/
|
|
30
|
+
- title: Query Guide
|
|
31
|
+
url: /query-guide/
|
|
32
|
+
- title: Examples
|
|
33
|
+
url: /examples/
|
|
34
|
+
- title: Skill Guide
|
|
35
|
+
url: /skill/
|
|
36
|
+
- title: GitHub
|
|
37
|
+
url: https://github.com/plelevier/gcyphrq
|
|
38
|
+
external: true
|
|
39
|
+
|
|
40
|
+
# Site settings
|
|
41
|
+
version: 0.11.0
|
|
42
|
+
node_version: ">=20"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<footer class="site-footer">
|
|
2
|
+
<div class="container">
|
|
3
|
+
<p>{{ site.title }} is released under the <a href="https://github.com/{{ site.repository }}/blob/main/LICENSE">MIT License</a>.</p>
|
|
4
|
+
<p>Built with <a href="https://graphology.github.io/">Graphology</a> and <a href="https://www.antlr.org/">ANTLR4</a>.</p>
|
|
5
|
+
</div>
|
|
6
|
+
</footer>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<meta name="description" content="{{ page.description | default: site.description }}">
|
|
2
|
+
<link rel="icon" href="{{ '/assets/favicon.svg' | relative_url }}" type="image/svg+xml">
|
|
3
|
+
|
|
4
|
+
<!-- Open Graph -->
|
|
5
|
+
<meta property="og:title" content="{% if page.title %}{{ page.title }} – {% endif %}{{ site.title }}">
|
|
6
|
+
<meta property="og:description" content="{{ page.description | default: site.description }}">
|
|
7
|
+
<meta property="og:type" content="website">
|
|
8
|
+
<meta property="og:url" content="{{ page.url | absolute_url }}">
|
|
9
|
+
|
|
10
|
+
<!-- Twitter -->
|
|
11
|
+
<meta name="twitter:card" content="summary">
|
|
12
|
+
<meta name="twitter:title" content="{% if page.title %}{{ page.title }} – {% endif %}{{ site.title }}">
|
|
13
|
+
<meta name="twitter:description" content="{{ page.description | default: site.description }}">
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<header class="site-header">
|
|
2
|
+
<div class="container">
|
|
3
|
+
<a href="{{ '/' | relative_url }}" class="logo">
|
|
4
|
+
<span class="logo-icon">◈</span>
|
|
5
|
+
<span class="logo-text">{{ site.title }} <small>v{{ site.version }}</small></span>
|
|
6
|
+
</a>
|
|
7
|
+
<nav class="main-nav">
|
|
8
|
+
<input type="checkbox" id="nav-toggle" class="nav-toggle">
|
|
9
|
+
<label for="nav-toggle" class="nav-toggle-label" aria-label="Toggle navigation">
|
|
10
|
+
<span></span><span></span><span></span>
|
|
11
|
+
</label>
|
|
12
|
+
<ul>
|
|
13
|
+
{% for item in site.nav %}
|
|
14
|
+
<li>
|
|
15
|
+
<a href="{{ item.url | relative_url }}" {% if item.external %}target="_blank" rel="noopener noreferrer"{% endif %} class="{% if page.url == item.url | relative_url %}active{% endif %}">
|
|
16
|
+
{{ item.title }}{% if item.external %} <span class="external-icon">↗</span>{% endif %}
|
|
17
|
+
</a>
|
|
18
|
+
</li>
|
|
19
|
+
{% endfor %}
|
|
20
|
+
</ul>
|
|
21
|
+
</nav>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% if page.title %}{{ page.title }} – {% endif %}{{ site.title }}</title>
|
|
7
|
+
<link rel="stylesheet" href="{{ '/assets/main.css' | relative_url }}">
|
|
8
|
+
{% include head.html %}
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
{% include nav.html %}
|
|
12
|
+
<main class="content">
|
|
13
|
+
<div class="container">
|
|
14
|
+
{{ content }}
|
|
15
|
+
</div>
|
|
16
|
+
</main>
|
|
17
|
+
{% include footer.html %}
|
|
18
|
+
<script src="{{ '/assets/cypher-highlight.js' | relative_url }}"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Lightweight Cypher keyword highlighter + copy buttons for code blocks
|
|
2
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3
|
+
// Highlight Cypher keywords
|
|
4
|
+
// Rouge wraps code blocks: div.language-cypher > div.highlight > pre.highlight > code
|
|
5
|
+
// We must only replace the <code> innerHTML to preserve the .highlight wrapper (background + CSS)
|
|
6
|
+
document.querySelectorAll('.language-cypher').forEach(el => {
|
|
7
|
+
const code = el.querySelector('code');
|
|
8
|
+
if (!code) return;
|
|
9
|
+
const text = code.textContent;
|
|
10
|
+
const tokens = tokenize(text);
|
|
11
|
+
code.innerHTML = tokens.map(t => {
|
|
12
|
+
if (t.type === 'keyword') return `<span class="cy-kw">${t.text}</span>`;
|
|
13
|
+
if (t.type === 'function') return `<span class="cy-fn">${t.text}</span>`;
|
|
14
|
+
if (t.type === 'label') return `<span class="cy-label">${t.text}</span>`;
|
|
15
|
+
if (t.type === 'reltype') return `<span class="cy-rel">${t.text}</span>`;
|
|
16
|
+
if (t.type === 'string') return `<span class="cy-str">${t.text}</span>`;
|
|
17
|
+
if (t.type === 'number') return `<span class="cy-num">${t.text}</span>`;
|
|
18
|
+
if (t.type === 'comment') return `<span class="cy-cmt">${t.text}</span>`;
|
|
19
|
+
return escapeHtml(t.text);
|
|
20
|
+
}).join('');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Add copy button to every code block
|
|
24
|
+
// Target the outermost .highlight div (the one that is a direct child of .highlighter-rouge)
|
|
25
|
+
document.querySelectorAll('.highlighter-rouge > .highlight').forEach(block => {
|
|
26
|
+
const btn = document.createElement('button');
|
|
27
|
+
btn.className = 'code-copy-btn';
|
|
28
|
+
btn.type = 'button';
|
|
29
|
+
btn.title = 'Copy';
|
|
30
|
+
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
|
|
31
|
+
|
|
32
|
+
// Wrap block + btn in a relative container
|
|
33
|
+
const wrapper = document.createElement('div');
|
|
34
|
+
wrapper.className = 'code-block-wrapper';
|
|
35
|
+
block.parentNode.replaceChild(wrapper, block);
|
|
36
|
+
wrapper.appendChild(block);
|
|
37
|
+
wrapper.appendChild(btn);
|
|
38
|
+
|
|
39
|
+
btn.addEventListener('click', () => {
|
|
40
|
+
const code = block.querySelector('code') || block;
|
|
41
|
+
const text = code.textContent.trim();
|
|
42
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
43
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
44
|
+
btn.classList.add('copied');
|
|
45
|
+
setTimeout(() => btn.classList.remove('copied'), 1500);
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
// Fallback for non-HTTPS (local dev)
|
|
49
|
+
const ta = document.createElement('textarea');
|
|
50
|
+
ta.value = text;
|
|
51
|
+
ta.style.position = 'fixed';
|
|
52
|
+
ta.style.opacity = '0';
|
|
53
|
+
document.body.appendChild(ta);
|
|
54
|
+
ta.select();
|
|
55
|
+
document.execCommand('copy');
|
|
56
|
+
document.body.removeChild(ta);
|
|
57
|
+
btn.classList.add('copied');
|
|
58
|
+
setTimeout(() => btn.classList.remove('copied'), 1500);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function tokenize(src) {
|
|
65
|
+
const tokens = [];
|
|
66
|
+
const keywords = new Set([
|
|
67
|
+
'MATCH', 'OPTIONAL', 'UNWIND', 'RETURN', 'WITH', 'WHERE',
|
|
68
|
+
'CREATE', 'MERGE', 'SET', 'DELETE', 'REMOVE', 'FOREACH',
|
|
69
|
+
'ORDER', 'BY', 'ASC', 'DESC', 'SKIP', 'LIMIT',
|
|
70
|
+
'ON', 'UPDATE',
|
|
71
|
+
'AND', 'OR', 'XOR', 'NOT', 'IN', 'IS', 'NULL', 'TRUE', 'FALSE',
|
|
72
|
+
'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
|
|
73
|
+
'STARTS', 'ENDS', 'CONTAINS',
|
|
74
|
+
]);
|
|
75
|
+
const functions = new Set([
|
|
76
|
+
'count', 'sum', 'min', 'max', 'avg', 'collect',
|
|
77
|
+
'head', 'tail', 'size', 'id', 'labels', 'properties',
|
|
78
|
+
'type', 'nodes', 'relationships',
|
|
79
|
+
'toString', 'toInt', 'toFloat', 'toBoolean',
|
|
80
|
+
'coalesce', 'exists',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < src.length) {
|
|
85
|
+
// Comments
|
|
86
|
+
if (src[i] === '/' && src[i + 1] === '/') {
|
|
87
|
+
let end = src.indexOf('\n', i);
|
|
88
|
+
if (end === -1) end = src.length;
|
|
89
|
+
tokens.push({ type: 'comment', text: src.slice(i, end) });
|
|
90
|
+
i = end;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strings (single or double quoted)
|
|
95
|
+
if (src[i] === "'" || src[i] === '"') {
|
|
96
|
+
const quote = src[i];
|
|
97
|
+
let j = i + 1;
|
|
98
|
+
while (j < src.length && src[j] !== quote) {
|
|
99
|
+
if (src[j] === '\\') j++; // skip escaped char
|
|
100
|
+
j++;
|
|
101
|
+
}
|
|
102
|
+
j++; // closing quote
|
|
103
|
+
tokens.push({ type: 'string', text: src.slice(i, j) });
|
|
104
|
+
i = j;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Numbers
|
|
109
|
+
if (/[0-9]/.test(src[i]) && (i === 0 || /[\s(,<>!=+\-*\/]/.test(src[i - 1]))) {
|
|
110
|
+
let j = i;
|
|
111
|
+
while (j < src.length && /[0-9.]/.test(src[j])) j++;
|
|
112
|
+
tokens.push({ type: 'number', text: src.slice(i, j) });
|
|
113
|
+
i = j;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Labels :Label
|
|
118
|
+
if (src[i] === ':' && i + 1 < src.length && /[A-Za-z_]/.test(src[i + 1])) {
|
|
119
|
+
let j = i + 1;
|
|
120
|
+
while (j < src.length && /[A-Za-z_0-9]/.test(src[j])) j++;
|
|
121
|
+
tokens.push({ type: 'label', text: src.slice(i, j) });
|
|
122
|
+
i = j;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Relationship types -[TYPE]-> or :TYPE
|
|
127
|
+
if (src[i] === '-' && src[i + 1] === '[') {
|
|
128
|
+
// Find the relationship type
|
|
129
|
+
let j = i + 2;
|
|
130
|
+
// skip whitespace
|
|
131
|
+
while (j < src.length && /\s/.test(src[j])) j++;
|
|
132
|
+
if (j < src.length && (src[j] === '>' || src[j] === '<' || /[A-Za-z_]/.test(src[j]))) {
|
|
133
|
+
if (/[A-Za-z_]/.test(src[j])) {
|
|
134
|
+
let k = j;
|
|
135
|
+
while (k < src.length && /[A-Za-z_0-9]/.test(src[k])) k++;
|
|
136
|
+
tokens.push({ type: 'text', text: src.slice(i, j) });
|
|
137
|
+
tokens.push({ type: 'reltype', text: src.slice(j, k) });
|
|
138
|
+
i = k;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Identifiers / keywords / functions
|
|
145
|
+
if (/[A-Za-z_]/.test(src[i])) {
|
|
146
|
+
let j = i;
|
|
147
|
+
while (j < src.length && /[A-Za-z_0-9]/.test(src[j])) j++;
|
|
148
|
+
const word = src.slice(i, j);
|
|
149
|
+
const upper = word.toUpperCase();
|
|
150
|
+
if (keywords.has(upper)) {
|
|
151
|
+
tokens.push({ type: 'keyword', text: word });
|
|
152
|
+
} else if (functions.has(word.toLowerCase()) && j < src.length && src[j] === '(') {
|
|
153
|
+
tokens.push({ type: 'function', text: word });
|
|
154
|
+
} else {
|
|
155
|
+
tokens.push({ type: 'text', text: word });
|
|
156
|
+
}
|
|
157
|
+
i = j;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Everything else
|
|
162
|
+
tokens.push({ type: 'text', text: src[i] });
|
|
163
|
+
i++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return tokens;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function escapeHtml(s) {
|
|
170
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
171
|
+
}
|