Skip to content

Commit 4dff3c6

Browse files
authored
Merge pull request #37 from code-hike/regex-range
Regex range
2 parents 173b620 + 87a94c0 commit 4dff3c6

15 files changed

+4949
-757
lines changed

.changeset/many-phones-wonder.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@code-hike/lighter": patch
3+
---
4+
5+
Add regex annotations

lib/dist/browser.esm.mjs

+1-1
Large diffs are not rendered by default.

lib/dist/index.cjs.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/dist/index.d.ts

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
type LineNumber = number;
2+
type ColumnNumber = number;
3+
type MultiLineRange = {
4+
fromLineNumber: LineNumber;
5+
toLineNumber: LineNumber;
6+
};
7+
type InlineRange = {
8+
lineNumber: LineNumber;
9+
fromColumn: ColumnNumber;
10+
toColumn: ColumnNumber;
11+
};
12+
type CodeRange = MultiLineRange | InlineRange;
13+
114
type RawTheme = {
215
name?: string;
316
type?: string;
@@ -30,19 +43,6 @@ type NamesTuple = typeof LANG_NAMES;
3043
type LanguageAlias = NamesTuple[number];
3144
type LanguageName = "abap" | "actionscript-3" | "ada" | "apache" | "apex" | "apl" | "applescript" | "ara" | "asm" | "astro" | "awk" | "ballerina" | "bat" | "beancount" | "berry" | "bibtex" | "bicep" | "blade" | "c" | "cadence" | "clarity" | "clojure" | "cmake" | "cobol" | "codeql" | "coffee" | "cpp" | "crystal" | "csharp" | "css" | "cue" | "cypher" | "d" | "dart" | "dax" | "diff" | "docker" | "dream-maker" | "elixir" | "elm" | "erb" | "erlang" | "fish" | "fsharp" | "gdresource" | "gdscript" | "gdshader" | "gherkin" | "git-commit" | "git-rebase" | "glimmer-js" | "glimmer-ts" | "glsl" | "gnuplot" | "go" | "graphql" | "groovy" | "hack" | "haml" | "handlebars" | "haskell" | "hcl" | "hjson" | "hlsl" | "html" | "http" | "imba" | "ini" | "java" | "javascript" | "jinja-html" | "jison" | "json" | "json5" | "jsonc" | "jsonl" | "jsonnet" | "jssm" | "jsx" | "julia" | "kotlin" | "kusto" | "latex" | "less" | "liquid" | "lisp" | "logo" | "lua" | "make" | "markdown" | "marko" | "matlab" | "mdx" | "mermaid" | "narrat" | "nextflow" | "nginx" | "nim" | "nix" | "objective-c" | "objective-cpp" | "ocaml" | "pascal" | "perl" | "php" | "plsql" | "postcss" | "powerquery" | "powershell" | "prisma" | "prolog" | "proto" | "pug" | "puppet" | "purescript" | "python" | "r" | "raku" | "razor" | "reg" | "rel" | "riscv" | "rst" | "ruby" | "rust" | "sas" | "sass" | "scala" | "scheme" | "scss" | "shaderlab" | "shellscript" | "shellsession" | "smalltalk" | "solidity" | "sparql" | "sql" | "ssh-config" | "stata" | "stylus" | "svelte" | "swift" | "system-verilog" | "tasl" | "tcl" | "tex" | "toml" | "tsx" | "turtle" | "twig" | "txt" | "typescript" | "v" | "vb" | "verilog" | "vhdl" | "viml" | "vue-html" | "vue" | "vyper" | "wasm" | "wenyan" | "wgsl" | "wolfram" | "xml" | "xsl" | "yaml" | "zenscript";
3245

33-
type LineNumber = number;
34-
type ColumnNumber = number;
35-
type MultiLineRange = {
36-
fromLineNumber: LineNumber;
37-
toLineNumber: LineNumber;
38-
};
39-
type InlineRange = {
40-
lineNumber: LineNumber;
41-
fromColumn: ColumnNumber;
42-
toColumn: ColumnNumber;
43-
};
44-
type CodeRange = MultiLineRange | InlineRange;
45-
4646
type Annotation = {
4747
name: string;
4848
query?: string;
@@ -121,7 +121,11 @@ declare function highlightSync(code: string, lang: LanguageAlias, themeOrThemeNa
121121
declare function highlightSync(code: string, lang: LanguageAlias, themeOrThemeName: Theme, config: AnnotatedConfig): AnnotatedLighterResult;
122122
declare function extractAnnotations(code: string, lang: LanguageAlias, annotationExtractor?: AnnotationExtractor): Promise<{
123123
code: string;
124-
annotations: Annotation[];
124+
annotations: {
125+
ranges: CodeRange[];
126+
name: string;
127+
query?: string;
128+
}[];
125129
}>;
126130
declare function getThemeColors(themeOrThemeName: Theme): Promise<{
127131
colorScheme: string;

lib/dist/index.esm.mjs

+1-1
Large diffs are not rendered by default.

lib/dist/worker.esm.mjs

+1-1
Large diffs are not rendered by default.

lib/src/comments.ts

+35-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Token } from "./annotations";
33
import { highlightText, highlightTokens } from "./highlighter";
44
import { CodeRange, parseRelativeRanges } from "./range";
55
import { FinalTheme } from "./theme";
6+
import { blockRegexToRange, inlineRegexToRange } from "./regex-range";
67

78
const PUNCTUATION = "#001";
89
const COMMENT = "#010";
@@ -38,6 +39,8 @@ export type AnnotationData = {
3839
query?: string;
3940
};
4041

42+
type RawAnnotation = AnnotationData & { lineNumber: number };
43+
4144
export type AnnotationExtractor =
4245
| string[]
4346
| ((comment: string) => null | AnnotationData);
@@ -52,7 +55,7 @@ export function extractCommentsFromCode(
5255
? highlightText(code)
5356
: highlightTokens(code, grammar, commentsTheme);
5457

55-
const allAnnotations: Annotation[] = [];
58+
const allAnnotations: RawAnnotation[] = [];
5659

5760
let lineNumber = 1;
5861
const newCode = lines
@@ -86,14 +89,24 @@ export function extractCommentsFromCode(
8689
.filter((line) => line !== null)
8790
.join(`\n`);
8891

89-
return { newCode, annotations: allAnnotations };
92+
const annotations = allAnnotations
93+
.map(({ rangeString, lineNumber, ...rest }) => ({
94+
...rest,
95+
ranges: parseRangeString(rangeString, lineNumber, newCode),
96+
}))
97+
.filter((a) => a.ranges.length > 0);
98+
99+
return { newCode, annotations };
90100
}
91101

92102
function getAnnotationsFromLine(
93103
tokens: Token[],
94104
annotationExtractor: AnnotationExtractor,
95105
lineNumber: number
96-
) {
106+
): {
107+
annotations: RawAnnotation[];
108+
lineWithoutComments: Token[] | null;
109+
} {
97110
// if no punctuation return empty
98111
if (!tokens.some((token) => token.style.color === PUNCTUATION)) {
99112
return { annotations: [], lineWithoutComments: tokens };
@@ -104,7 +117,8 @@ function getAnnotationsFromLine(
104117
tokens: Token[];
105118
name: string;
106119
query?: string;
107-
ranges: CodeRange[];
120+
rangeString: string;
121+
lineNumber: number;
108122
}[] = [];
109123
let i = 0;
110124
while (i < tokens.length) {
@@ -144,7 +158,8 @@ function getAnnotationsFromLine(
144158
tokens: commentTokens,
145159
name,
146160
query,
147-
ranges: parseRelativeRanges(rangeString, lineNumber),
161+
rangeString,
162+
lineNumber,
148163
});
149164

150165
i += 2;
@@ -165,12 +180,26 @@ function getAnnotationsFromLine(
165180
annotations: comments.map((a) => ({
166181
name: a.name,
167182
query: a.query,
168-
ranges: a.ranges,
183+
lineNumber: a.lineNumber,
184+
rangeString: a.rangeString,
169185
})),
170186
lineWithoutComments: newLine,
171187
};
172188
}
173189

190+
function parseRangeString(
191+
rangeString: string,
192+
lineNumber: number,
193+
code: string
194+
) {
195+
if (rangeString && rangeString.startsWith("(/")) {
196+
return blockRegexToRange(code, rangeString, lineNumber);
197+
} else if (rangeString && rangeString.startsWith("[/")) {
198+
return inlineRegexToRange(code, rangeString, lineNumber);
199+
}
200+
return parseRelativeRanges(rangeString, lineNumber);
201+
}
202+
174203
function getAnnotationDataFromNames(content: string, names: string[]) {
175204
const regex = /\s*([\w-]+)?(\([^\)]*\)|\[[^\]]*\])?(.*)$/;
176205
const match = content.match(regex);

lib/src/extractor.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { test, expect } from "vitest";
2+
import { extractor } from "./extractor";
3+
4+
test("extraction", () => {
5+
expectExtraction("!foo", ["foo", , ""]);
6+
expectExtraction("!foo(1)", ["foo", "(1)", ""]);
7+
expectExtraction("!foo[1]", ["foo", "[1]", ""]);
8+
expectExtraction("!foo[ ] q q", ["foo", "[ ]", "q q"]);
9+
expectExtraction("!foo(/bar/)", ["foo", "(/bar/)", ""]);
10+
expectExtraction("!foo(/bar baz/g) 1 2", ["foo", "(/bar baz/g)", "1 2"]);
11+
expectExtraction("!Focus(/y/) bar", ["Focus", "(/y/)", "bar"]);
12+
});
13+
14+
function expectExtraction(comment: string, expected: [string, string, string]) {
15+
const result = extractor(comment);
16+
const [name, rangeString, query] = expected;
17+
expect(result).toEqual({ name, rangeString, query });
18+
}

lib/src/extractor.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function extractor(comment: string, prefix: string = "!") {
2+
// const regex = /\s*(!?[\w-]+)?(\([^\)]*\)|\[[^\]]*\])?(.*)$/;
3+
4+
const regex = new RegExp(
5+
`\\s*(${prefix}?[\\w-]+)?(\\([^\\)]*\\)|\\[[^\\]]*\\])?(.*)$`
6+
);
7+
const match = comment.match(regex);
8+
const name = match[1];
9+
const rangeString = match[2];
10+
const query = match[3]?.trim();
11+
if (!name || !name.startsWith(prefix)) {
12+
return null;
13+
}
14+
return {
15+
name: name.slice(prefix.length),
16+
rangeString,
17+
query,
18+
};
19+
}

lib/src/regex-range.test.ts

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { expect, test } from "vitest";
2+
import { blockRegexToRange, inlineRegexToRange } from "./regex-range";
3+
4+
test("block regex range", () => {
5+
const code = `
6+
function x() {
7+
console.log(1)
8+
console.log(2)
9+
if (true) {
10+
console.log(3)
11+
}
12+
}
13+
14+
function y() {
15+
console.log(1)
16+
}
17+
`.trim();
18+
const result = blockRegexToRange(code, "(/^ {2}.*(?:\n {2}.*)*/gm)", 1);
19+
expect(result).toMatchInlineSnapshot(`
20+
[
21+
{
22+
"fromLineNumber": 2,
23+
"toLineNumber": 6,
24+
},
25+
{
26+
"fromLineNumber": 10,
27+
"toLineNumber": 10,
28+
},
29+
]
30+
`);
31+
});
32+
33+
test("block regex range repeated line", () => {
34+
const code = `
35+
const foo = 1;
36+
const bar = 2
37+
foo = foo + bar;
38+
`.trim();
39+
const result = blockRegexToRange(code, "(/foo/g)", 1);
40+
expect(result).toMatchInlineSnapshot(`
41+
[
42+
{
43+
"fromLineNumber": 1,
44+
"toLineNumber": 1,
45+
},
46+
{
47+
"fromLineNumber": 3,
48+
"toLineNumber": 3,
49+
},
50+
]
51+
`);
52+
});
53+
54+
test("inline regex range", () => {
55+
const code = `
56+
const foo = 1;
57+
const bar = 2
58+
foo = foo + bar;
59+
`.trim();
60+
const result = inlineRegexToRange(code, "[/foo/g]", 1);
61+
62+
expect(result).toMatchInlineSnapshot(`
63+
[
64+
{
65+
"fromColumn": 7,
66+
"lineNumber": 1,
67+
"toColumn": 9,
68+
},
69+
]
70+
`);
71+
});
72+
73+
test("inline regex range with capture group", () => {
74+
const code = `
75+
function C() {
76+
return <div className="bg-red-500">
77+
<span className="text-blue-500"><a className="x">Hello</a></span>
78+
</div>
79+
}
80+
`.trim();
81+
const result = inlineRegexToRange(code, '[/className="(.*?)"/gm]', 1);
82+
83+
expect(result).toMatchInlineSnapshot(`
84+
[
85+
{
86+
"fromColumn": 26,
87+
"lineNumber": 2,
88+
"toColumn": 35,
89+
},
90+
{
91+
"fromColumn": 22,
92+
"lineNumber": 3,
93+
"toColumn": 34,
94+
},
95+
{
96+
"fromColumn": 51,
97+
"lineNumber": 3,
98+
"toColumn": 51,
99+
},
100+
]
101+
`);
102+
});
103+
104+
test("inline regex range with capture group not g", () => {
105+
const code = `
106+
function C() {
107+
return <div className="">
108+
<span className="text-blue-500"><a className="x">Hello</a></span>
109+
</div>
110+
}
111+
`.trim();
112+
const result = inlineRegexToRange(code, '[/className="(.*?)"/]', 3);
113+
114+
expect(result).toMatchInlineSnapshot(`
115+
[
116+
{
117+
"fromColumn": 22,
118+
"lineNumber": 3,
119+
"toColumn": 34,
120+
},
121+
]
122+
`);
123+
});

0 commit comments

Comments
 (0)