This is page 3 of 10. Use http://codebase.md/moisnx/arc?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .clang-format
├── .config
│ └── arceditor
│ ├── config.yaml
│ ├── keybinds.conf
│ └── themes
│ ├── catppuccin-mocha.theme
│ ├── cyberpunk-neon.theme
│ ├── default.theme
│ ├── dracula.theme
│ ├── github_dark.theme
│ ├── gruvbox_dark.theme
│ ├── gruvbox_light.theme
│ ├── high_constrast_dark.theme
│ ├── monokai.theme
│ ├── onedark.theme
│ ├── solarized_dark.theme
│ ├── solarized_light.theme
│ ├── tokyo_night.theme
│ └── vscode_light.theme
├── .github
│ └── assets
│ └── screenshot.gif
├── .gitignore
├── .gitmessage
├── .gitmodules
├── build.md
├── CMakeLists.txt
├── deps
│ └── tree-sitter-markdown
│ ├── .editorconfig
│ ├── .gitattributes
│ ├── .github
│ │ ├── screenshot.png
│ │ └── workflows
│ │ ├── ci.yml
│ │ ├── publish.yml
│ │ └── release.yml
│ ├── .gitignore
│ ├── binding.gyp
│ ├── bindings
│ │ ├── go
│ │ │ ├── binding_test.go
│ │ │ ├── markdown_inline.go
│ │ │ └── markdown.go
│ │ ├── node
│ │ │ ├── binding_test.js
│ │ │ ├── binding.cc
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ └── inline.js
│ │ ├── python
│ │ │ ├── tests
│ │ │ │ └── test_binding.py
│ │ │ └── tree_sitter_markdown
│ │ │ ├── __init__.py
│ │ │ ├── __init__.pyi
│ │ │ ├── binding.c
│ │ │ └── py.typed
│ │ ├── rust
│ │ │ ├── benchmark.rs
│ │ │ ├── build.rs
│ │ │ ├── lib.rs
│ │ │ └── parser.rs
│ │ └── swift
│ │ ├── .gitignore
│ │ └── TreeSitterMarkdownTests
│ │ └── TreeSitterMarkdownTests.swift
│ ├── Cargo.toml
│ ├── CMakeLists.txt
│ ├── common
│ │ ├── common.js
│ │ ├── common.mak
│ │ └── html_entities.json
│ ├── CONTRIBUTING.md
│ ├── go.mod
│ ├── LICENSE
│ ├── Makefile
│ ├── package-lock.json
│ ├── package.json
│ ├── Package.resolved
│ ├── Package.swift
│ ├── pyproject.toml
│ ├── README.md
│ ├── scripts
│ │ ├── build.js
│ │ └── test.js
│ ├── setup.py
│ ├── tree-sitter-markdown
│ │ ├── bindings
│ │ │ ├── c
│ │ │ │ ├── tree-sitter-markdown.h
│ │ │ │ └── tree-sitter-markdown.pc.in
│ │ │ └── swift
│ │ │ └── TreeSitterMarkdown
│ │ │ └── markdown.h
│ │ ├── CMakeLists.txt
│ │ ├── grammar.js
│ │ ├── Makefile
│ │ ├── package.json
│ │ ├── queries
│ │ │ ├── highlights.scm
│ │ │ └── injections.scm
│ │ ├── src
│ │ │ ├── grammar.json
│ │ │ ├── node-types.json
│ │ │ ├── parser.c
│ │ │ ├── scanner.c
│ │ │ └── tree_sitter
│ │ │ ├── alloc.h
│ │ │ ├── array.h
│ │ │ └── parser.h
│ │ └── test
│ │ └── corpus
│ │ ├── extension_minus_metadata.txt
│ │ ├── extension_pipe_table.txt
│ │ ├── extension_plus_metadata.txt
│ │ ├── extension_task_list.txt
│ │ ├── failing.txt
│ │ ├── issues.txt
│ │ └── spec.txt
│ ├── tree-sitter-markdown-inline
│ │ ├── bindings
│ │ │ ├── c
│ │ │ │ ├── tree-sitter-markdown-inline.h
│ │ │ │ └── tree-sitter-markdown-inline.pc.in
│ │ │ └── swift
│ │ │ └── TreeSitterMarkdownInline
│ │ │ └── markdown_inline.h
│ │ ├── CMakeLists.txt
│ │ ├── grammar.js
│ │ ├── Makefile
│ │ ├── package.json
│ │ ├── queries
│ │ │ ├── highlights.scm
│ │ │ └── injections.scm
│ │ ├── src
│ │ │ ├── grammar.json
│ │ │ ├── node-types.json
│ │ │ ├── parser.c
│ │ │ ├── scanner.c
│ │ │ └── tree_sitter
│ │ │ ├── alloc.h
│ │ │ ├── array.h
│ │ │ └── parser.h
│ │ └── test
│ │ └── corpus
│ │ ├── extension_latex.txt
│ │ ├── extension_strikethrough.txt
│ │ ├── extension_wikilink.txt
│ │ ├── failing.txt
│ │ ├── issues.txt
│ │ ├── spec.txt
│ │ └── tags.txt
│ └── tree-sitter.json
├── LICENSE
├── Makefile
├── quickstart.md
├── README.md
├── src
│ ├── core
│ │ ├── buffer.cpp
│ │ ├── buffer.h
│ │ ├── config_manager.cpp
│ │ ├── config_manager.h
│ │ ├── editor_delta.h
│ │ ├── editor_validation.h
│ │ ├── editor.cpp
│ │ └── editor.h
│ ├── features
│ │ ├── markdown_state.h
│ │ ├── syntax_config_loader.cpp
│ │ ├── syntax_config_loader.h
│ │ ├── syntax_highlighter.cpp
│ │ └── syntax_highlighter.h
│ ├── main.cpp
│ └── ui
│ ├── input_handler.cpp
│ ├── input_handler.h
│ ├── renderer.cpp
│ ├── renderer.h
│ ├── style_manager.cpp
│ └── style_manager.h
└── treesitter
├── languages.yaml
└── queries
├── _javascript
│ ├── highlights.scm
│ ├── locals.scm
│ └── tags.scm
├── _jsx
│ ├── highlights.scm
│ ├── indents.scm
│ └── textobjects.scm
├── _typescript
│ ├── highlights.scm
│ ├── indents.scm
│ ├── locals.scm
│ ├── tags.scm
│ └── textobjects.scm
├── bash
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── c
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── cpp
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── css
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ └── rainbows.scm
├── ecma
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── README.md
│ └── textobjects.scm
├── go
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── javascript
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── markdown
│ ├── highlights.scm
│ ├── injections.scm
│ └── tags.scm
├── markdown.inline
│ ├── highlights.scm
│ └── injections.scm
├── python
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── rust
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── toml
│ ├── highlights.scm
│ ├── injections.scm
│ ├── rainbows.scm
│ └── textobjects.scm
├── tsx
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── typescript
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── locals.scm
│ ├── rainbows.scm
│ ├── tags.scm
│ └── textobjects.scm
├── yaml
│ ├── highlights.scm
│ ├── indents.scm
│ ├── injections.scm
│ ├── rainbows.scm
│ └── textobjects.scm
└── zig
├── highlights.scm
├── indents.scm
├── injections.scm
└── textobjects.scm
```
# Files
--------------------------------------------------------------------------------
/deps/tree-sitter-markdown/tree-sitter-markdown/src/node-types.json:
--------------------------------------------------------------------------------
```json
1 | [
2 | {
3 | "type": "atx_heading",
4 | "named": true,
5 | "fields": {
6 | "heading_content": {
7 | "multiple": false,
8 | "required": false,
9 | "types": [
10 | {
11 | "type": "inline",
12 | "named": true
13 | }
14 | ]
15 | }
16 | },
17 | "children": {
18 | "multiple": true,
19 | "required": true,
20 | "types": [
21 | {
22 | "type": "atx_h1_marker",
23 | "named": true
24 | },
25 | {
26 | "type": "atx_h2_marker",
27 | "named": true
28 | },
29 | {
30 | "type": "atx_h3_marker",
31 | "named": true
32 | },
33 | {
34 | "type": "atx_h4_marker",
35 | "named": true
36 | },
37 | {
38 | "type": "atx_h5_marker",
39 | "named": true
40 | },
41 | {
42 | "type": "atx_h6_marker",
43 | "named": true
44 | },
45 | {
46 | "type": "block_continuation",
47 | "named": true
48 | }
49 | ]
50 | }
51 | },
52 | {
53 | "type": "backslash_escape",
54 | "named": true,
55 | "fields": {}
56 | },
57 | {
58 | "type": "block_quote",
59 | "named": true,
60 | "fields": {},
61 | "children": {
62 | "multiple": true,
63 | "required": true,
64 | "types": [
65 | {
66 | "type": "block_continuation",
67 | "named": true
68 | },
69 | {
70 | "type": "block_quote",
71 | "named": true
72 | },
73 | {
74 | "type": "block_quote_marker",
75 | "named": true
76 | },
77 | {
78 | "type": "fenced_code_block",
79 | "named": true
80 | },
81 | {
82 | "type": "html_block",
83 | "named": true
84 | },
85 | {
86 | "type": "indented_code_block",
87 | "named": true
88 | },
89 | {
90 | "type": "link_reference_definition",
91 | "named": true
92 | },
93 | {
94 | "type": "list",
95 | "named": true
96 | },
97 | {
98 | "type": "paragraph",
99 | "named": true
100 | },
101 | {
102 | "type": "pipe_table",
103 | "named": true
104 | },
105 | {
106 | "type": "section",
107 | "named": true
108 | },
109 | {
110 | "type": "setext_heading",
111 | "named": true
112 | },
113 | {
114 | "type": "thematic_break",
115 | "named": true
116 | }
117 | ]
118 | }
119 | },
120 | {
121 | "type": "code_fence_content",
122 | "named": true,
123 | "fields": {},
124 | "children": {
125 | "multiple": true,
126 | "required": false,
127 | "types": [
128 | {
129 | "type": "block_continuation",
130 | "named": true
131 | }
132 | ]
133 | }
134 | },
135 | {
136 | "type": "document",
137 | "named": true,
138 | "root": true,
139 | "fields": {},
140 | "children": {
141 | "multiple": true,
142 | "required": false,
143 | "types": [
144 | {
145 | "type": "minus_metadata",
146 | "named": true
147 | },
148 | {
149 | "type": "plus_metadata",
150 | "named": true
151 | },
152 | {
153 | "type": "section",
154 | "named": true
155 | }
156 | ]
157 | }
158 | },
159 | {
160 | "type": "fenced_code_block",
161 | "named": true,
162 | "fields": {},
163 | "children": {
164 | "multiple": true,
165 | "required": true,
166 | "types": [
167 | {
168 | "type": "block_continuation",
169 | "named": true
170 | },
171 | {
172 | "type": "code_fence_content",
173 | "named": true
174 | },
175 | {
176 | "type": "fenced_code_block_delimiter",
177 | "named": true
178 | },
179 | {
180 | "type": "info_string",
181 | "named": true
182 | }
183 | ]
184 | }
185 | },
186 | {
187 | "type": "html_block",
188 | "named": true,
189 | "fields": {},
190 | "children": {
191 | "multiple": true,
192 | "required": false,
193 | "types": [
194 | {
195 | "type": "block_continuation",
196 | "named": true
197 | }
198 | ]
199 | }
200 | },
201 | {
202 | "type": "indented_code_block",
203 | "named": true,
204 | "fields": {},
205 | "children": {
206 | "multiple": true,
207 | "required": false,
208 | "types": [
209 | {
210 | "type": "block_continuation",
211 | "named": true
212 | }
213 | ]
214 | }
215 | },
216 | {
217 | "type": "info_string",
218 | "named": true,
219 | "fields": {},
220 | "children": {
221 | "multiple": true,
222 | "required": false,
223 | "types": [
224 | {
225 | "type": "backslash_escape",
226 | "named": true
227 | },
228 | {
229 | "type": "entity_reference",
230 | "named": true
231 | },
232 | {
233 | "type": "language",
234 | "named": true
235 | },
236 | {
237 | "type": "numeric_character_reference",
238 | "named": true
239 | }
240 | ]
241 | }
242 | },
243 | {
244 | "type": "inline",
245 | "named": true,
246 | "fields": {},
247 | "children": {
248 | "multiple": true,
249 | "required": false,
250 | "types": [
251 | {
252 | "type": "block_continuation",
253 | "named": true
254 | }
255 | ]
256 | }
257 | },
258 | {
259 | "type": "language",
260 | "named": true,
261 | "fields": {},
262 | "children": {
263 | "multiple": true,
264 | "required": false,
265 | "types": [
266 | {
267 | "type": "backslash_escape",
268 | "named": true
269 | },
270 | {
271 | "type": "entity_reference",
272 | "named": true
273 | },
274 | {
275 | "type": "numeric_character_reference",
276 | "named": true
277 | }
278 | ]
279 | }
280 | },
281 | {
282 | "type": "link_destination",
283 | "named": true,
284 | "fields": {},
285 | "children": {
286 | "multiple": true,
287 | "required": false,
288 | "types": [
289 | {
290 | "type": "backslash_escape",
291 | "named": true
292 | },
293 | {
294 | "type": "entity_reference",
295 | "named": true
296 | },
297 | {
298 | "type": "numeric_character_reference",
299 | "named": true
300 | }
301 | ]
302 | }
303 | },
304 | {
305 | "type": "link_label",
306 | "named": true,
307 | "fields": {},
308 | "children": {
309 | "multiple": true,
310 | "required": false,
311 | "types": [
312 | {
313 | "type": "backslash_escape",
314 | "named": true
315 | },
316 | {
317 | "type": "block_continuation",
318 | "named": true
319 | },
320 | {
321 | "type": "entity_reference",
322 | "named": true
323 | },
324 | {
325 | "type": "numeric_character_reference",
326 | "named": true
327 | }
328 | ]
329 | }
330 | },
331 | {
332 | "type": "link_reference_definition",
333 | "named": true,
334 | "fields": {},
335 | "children": {
336 | "multiple": true,
337 | "required": true,
338 | "types": [
339 | {
340 | "type": "block_continuation",
341 | "named": true
342 | },
343 | {
344 | "type": "link_destination",
345 | "named": true
346 | },
347 | {
348 | "type": "link_label",
349 | "named": true
350 | },
351 | {
352 | "type": "link_title",
353 | "named": true
354 | }
355 | ]
356 | }
357 | },
358 | {
359 | "type": "link_title",
360 | "named": true,
361 | "fields": {},
362 | "children": {
363 | "multiple": true,
364 | "required": false,
365 | "types": [
366 | {
367 | "type": "backslash_escape",
368 | "named": true
369 | },
370 | {
371 | "type": "block_continuation",
372 | "named": true
373 | },
374 | {
375 | "type": "entity_reference",
376 | "named": true
377 | },
378 | {
379 | "type": "numeric_character_reference",
380 | "named": true
381 | }
382 | ]
383 | }
384 | },
385 | {
386 | "type": "list",
387 | "named": true,
388 | "fields": {},
389 | "children": {
390 | "multiple": true,
391 | "required": false,
392 | "types": [
393 | {
394 | "type": "list_item",
395 | "named": true
396 | }
397 | ]
398 | }
399 | },
400 | {
401 | "type": "list_item",
402 | "named": true,
403 | "fields": {},
404 | "children": {
405 | "multiple": true,
406 | "required": true,
407 | "types": [
408 | {
409 | "type": "block_continuation",
410 | "named": true
411 | },
412 | {
413 | "type": "block_quote",
414 | "named": true
415 | },
416 | {
417 | "type": "fenced_code_block",
418 | "named": true
419 | },
420 | {
421 | "type": "html_block",
422 | "named": true
423 | },
424 | {
425 | "type": "indented_code_block",
426 | "named": true
427 | },
428 | {
429 | "type": "link_reference_definition",
430 | "named": true
431 | },
432 | {
433 | "type": "list",
434 | "named": true
435 | },
436 | {
437 | "type": "list_marker_dot",
438 | "named": true
439 | },
440 | {
441 | "type": "list_marker_minus",
442 | "named": true
443 | },
444 | {
445 | "type": "list_marker_parenthesis",
446 | "named": true
447 | },
448 | {
449 | "type": "list_marker_plus",
450 | "named": true
451 | },
452 | {
453 | "type": "list_marker_star",
454 | "named": true
455 | },
456 | {
457 | "type": "paragraph",
458 | "named": true
459 | },
460 | {
461 | "type": "pipe_table",
462 | "named": true
463 | },
464 | {
465 | "type": "section",
466 | "named": true
467 | },
468 | {
469 | "type": "setext_heading",
470 | "named": true
471 | },
472 | {
473 | "type": "task_list_marker_checked",
474 | "named": true
475 | },
476 | {
477 | "type": "task_list_marker_unchecked",
478 | "named": true
479 | },
480 | {
481 | "type": "thematic_break",
482 | "named": true
483 | }
484 | ]
485 | }
486 | },
487 | {
488 | "type": "list_marker_dot",
489 | "named": true,
490 | "fields": {}
491 | },
492 | {
493 | "type": "list_marker_minus",
494 | "named": true,
495 | "fields": {}
496 | },
497 | {
498 | "type": "list_marker_parenthesis",
499 | "named": true,
500 | "fields": {}
501 | },
502 | {
503 | "type": "list_marker_plus",
504 | "named": true,
505 | "fields": {}
506 | },
507 | {
508 | "type": "list_marker_star",
509 | "named": true,
510 | "fields": {}
511 | },
512 | {
513 | "type": "paragraph",
514 | "named": true,
515 | "fields": {},
516 | "children": {
517 | "multiple": true,
518 | "required": true,
519 | "types": [
520 | {
521 | "type": "block_continuation",
522 | "named": true
523 | },
524 | {
525 | "type": "inline",
526 | "named": true
527 | }
528 | ]
529 | }
530 | },
531 | {
532 | "type": "pipe_table",
533 | "named": true,
534 | "fields": {},
535 | "children": {
536 | "multiple": true,
537 | "required": true,
538 | "types": [
539 | {
540 | "type": "block_continuation",
541 | "named": true
542 | },
543 | {
544 | "type": "pipe_table_delimiter_row",
545 | "named": true
546 | },
547 | {
548 | "type": "pipe_table_header",
549 | "named": true
550 | },
551 | {
552 | "type": "pipe_table_row",
553 | "named": true
554 | }
555 | ]
556 | }
557 | },
558 | {
559 | "type": "pipe_table_cell",
560 | "named": true,
561 | "fields": {}
562 | },
563 | {
564 | "type": "pipe_table_delimiter_cell",
565 | "named": true,
566 | "fields": {},
567 | "children": {
568 | "multiple": true,
569 | "required": false,
570 | "types": [
571 | {
572 | "type": "pipe_table_align_left",
573 | "named": true
574 | },
575 | {
576 | "type": "pipe_table_align_right",
577 | "named": true
578 | }
579 | ]
580 | }
581 | },
582 | {
583 | "type": "pipe_table_delimiter_row",
584 | "named": true,
585 | "fields": {},
586 | "children": {
587 | "multiple": true,
588 | "required": true,
589 | "types": [
590 | {
591 | "type": "pipe_table_delimiter_cell",
592 | "named": true
593 | }
594 | ]
595 | }
596 | },
597 | {
598 | "type": "pipe_table_header",
599 | "named": true,
600 | "fields": {},
601 | "children": {
602 | "multiple": true,
603 | "required": true,
604 | "types": [
605 | {
606 | "type": "pipe_table_cell",
607 | "named": true
608 | }
609 | ]
610 | }
611 | },
612 | {
613 | "type": "pipe_table_row",
614 | "named": true,
615 | "fields": {},
616 | "children": {
617 | "multiple": true,
618 | "required": true,
619 | "types": [
620 | {
621 | "type": "pipe_table_cell",
622 | "named": true
623 | }
624 | ]
625 | }
626 | },
627 | {
628 | "type": "section",
629 | "named": true,
630 | "fields": {},
631 | "children": {
632 | "multiple": true,
633 | "required": false,
634 | "types": [
635 | {
636 | "type": "atx_heading",
637 | "named": true
638 | },
639 | {
640 | "type": "block_continuation",
641 | "named": true
642 | },
643 | {
644 | "type": "block_quote",
645 | "named": true
646 | },
647 | {
648 | "type": "fenced_code_block",
649 | "named": true
650 | },
651 | {
652 | "type": "html_block",
653 | "named": true
654 | },
655 | {
656 | "type": "indented_code_block",
657 | "named": true
658 | },
659 | {
660 | "type": "link_reference_definition",
661 | "named": true
662 | },
663 | {
664 | "type": "list",
665 | "named": true
666 | },
667 | {
668 | "type": "paragraph",
669 | "named": true
670 | },
671 | {
672 | "type": "pipe_table",
673 | "named": true
674 | },
675 | {
676 | "type": "section",
677 | "named": true
678 | },
679 | {
680 | "type": "setext_heading",
681 | "named": true
682 | },
683 | {
684 | "type": "thematic_break",
685 | "named": true
686 | }
687 | ]
688 | }
689 | },
690 | {
691 | "type": "setext_heading",
692 | "named": true,
693 | "fields": {
694 | "heading_content": {
695 | "multiple": false,
696 | "required": true,
697 | "types": [
698 | {
699 | "type": "paragraph",
700 | "named": true
701 | }
702 | ]
703 | }
704 | },
705 | "children": {
706 | "multiple": true,
707 | "required": true,
708 | "types": [
709 | {
710 | "type": "block_continuation",
711 | "named": true
712 | },
713 | {
714 | "type": "setext_h1_underline",
715 | "named": true
716 | },
717 | {
718 | "type": "setext_h2_underline",
719 | "named": true
720 | }
721 | ]
722 | }
723 | },
724 | {
725 | "type": "task_list_marker_checked",
726 | "named": true,
727 | "fields": {}
728 | },
729 | {
730 | "type": "task_list_marker_unchecked",
731 | "named": true,
732 | "fields": {}
733 | },
734 | {
735 | "type": "thematic_break",
736 | "named": true,
737 | "fields": {},
738 | "children": {
739 | "multiple": false,
740 | "required": false,
741 | "types": [
742 | {
743 | "type": "block_continuation",
744 | "named": true
745 | }
746 | ]
747 | }
748 | },
749 | {
750 | "type": "!",
751 | "named": false
752 | },
753 | {
754 | "type": "\"",
755 | "named": false
756 | },
757 | {
758 | "type": "#",
759 | "named": false
760 | },
761 | {
762 | "type": "$",
763 | "named": false
764 | },
765 | {
766 | "type": "%",
767 | "named": false
768 | },
769 | {
770 | "type": "&",
771 | "named": false
772 | },
773 | {
774 | "type": "'",
775 | "named": false
776 | },
777 | {
778 | "type": "(",
779 | "named": false
780 | },
781 | {
782 | "type": ")",
783 | "named": false
784 | },
785 | {
786 | "type": "*",
787 | "named": false
788 | },
789 | {
790 | "type": "+",
791 | "named": false
792 | },
793 | {
794 | "type": ",",
795 | "named": false
796 | },
797 | {
798 | "type": "-",
799 | "named": false
800 | },
801 | {
802 | "type": "-->",
803 | "named": false
804 | },
805 | {
806 | "type": ".",
807 | "named": false
808 | },
809 | {
810 | "type": "/",
811 | "named": false
812 | },
813 | {
814 | "type": ":",
815 | "named": false
816 | },
817 | {
818 | "type": ";",
819 | "named": false
820 | },
821 | {
822 | "type": "<",
823 | "named": false
824 | },
825 | {
826 | "type": "=",
827 | "named": false
828 | },
829 | {
830 | "type": ">",
831 | "named": false
832 | },
833 | {
834 | "type": "?",
835 | "named": false
836 | },
837 | {
838 | "type": "?>",
839 | "named": false
840 | },
841 | {
842 | "type": "@",
843 | "named": false
844 | },
845 | {
846 | "type": "[",
847 | "named": false
848 | },
849 | {
850 | "type": "\\",
851 | "named": false
852 | },
853 | {
854 | "type": "]",
855 | "named": false
856 | },
857 | {
858 | "type": "]]>",
859 | "named": false
860 | },
861 | {
862 | "type": "^",
863 | "named": false
864 | },
865 | {
866 | "type": "_",
867 | "named": false
868 | },
869 | {
870 | "type": "`",
871 | "named": false
872 | },
873 | {
874 | "type": "atx_h1_marker",
875 | "named": true
876 | },
877 | {
878 | "type": "atx_h2_marker",
879 | "named": true
880 | },
881 | {
882 | "type": "atx_h3_marker",
883 | "named": true
884 | },
885 | {
886 | "type": "atx_h4_marker",
887 | "named": true
888 | },
889 | {
890 | "type": "atx_h5_marker",
891 | "named": true
892 | },
893 | {
894 | "type": "atx_h6_marker",
895 | "named": true
896 | },
897 | {
898 | "type": "block_continuation",
899 | "named": true
900 | },
901 | {
902 | "type": "block_quote_marker",
903 | "named": true
904 | },
905 | {
906 | "type": "entity_reference",
907 | "named": true
908 | },
909 | {
910 | "type": "fenced_code_block_delimiter",
911 | "named": true
912 | },
913 | {
914 | "type": "minus_metadata",
915 | "named": true
916 | },
917 | {
918 | "type": "numeric_character_reference",
919 | "named": true
920 | },
921 | {
922 | "type": "pipe_table_align_left",
923 | "named": true
924 | },
925 | {
926 | "type": "pipe_table_align_right",
927 | "named": true
928 | },
929 | {
930 | "type": "plus_metadata",
931 | "named": true
932 | },
933 | {
934 | "type": "setext_h1_underline",
935 | "named": true
936 | },
937 | {
938 | "type": "setext_h2_underline",
939 | "named": true
940 | },
941 | {
942 | "type": "{",
943 | "named": false
944 | },
945 | {
946 | "type": "|",
947 | "named": false
948 | },
949 | {
950 | "type": "}",
951 | "named": false
952 | },
953 | {
954 | "type": "~",
955 | "named": false
956 | }
957 | ]
```
--------------------------------------------------------------------------------
/src/core/config_manager.cpp:
--------------------------------------------------------------------------------
```cpp
1 | #include "config_manager.h"
2 | #include <algorithm>
3 | #include <cstdlib>
4 | #include <filesystem>
5 | #include <fstream>
6 | #include <functional>
7 | #include <iostream>
8 | #include <memory>
9 | #include <sstream>
10 |
11 | #include <yaml-cpp/yaml.h>
12 |
13 | // EFSW includes
14 | #include <efsw/efsw.h>
15 | #include <efsw/efsw.hpp>
16 |
17 | namespace fs = std::filesystem;
18 |
19 | // Static initialization
20 | std::string ConfigManager::config_dir_cache_;
21 | std::string ConfigManager::active_theme_ = "default";
22 | std::vector<ConfigReloadCallback> ConfigManager::reload_callbacks_;
23 | std::unique_ptr<efsw::FileWatchListener> ConfigManager::watcher_listener_ =
24 | nullptr;
25 | std::unique_ptr<efsw::FileWatcher> ConfigManager::watcher_instance_ = nullptr;
26 | std::atomic<bool> ConfigManager::reload_pending_ = false;
27 | EditorConfig ConfigManager::editor_config_;
28 | SyntaxConfig ConfigManager::syntax_config_;
29 |
30 | // --- EFSW Listener Class ---
31 |
32 | // We define a custom listener that efsw will use to communicate events.
33 | class ConfigFileListener : public efsw::FileWatchListener
34 | {
35 | public:
36 | void handleFileAction(efsw::WatchID watchid, const std::string &dir,
37 | const std::string &filename, efsw::Action action,
38 | std::string oldFilename) override
39 | {
40 | // We are interested in Modification and Renaming (e.g., atomic save by
41 | // editor)
42 | if (filename == "config.yaml" &&
43 | (action == efsw::Action::Modified || action == efsw::Action::Moved))
44 | {
45 | // Call the static handler in ConfigManager
46 | ConfigManager::handleFileChange();
47 | }
48 | }
49 | };
50 |
51 | // -----------------------------------------------------------------
52 | // CONFIG DIRECTORY AND PATH GETTERS
53 | // -----------------------------------------------------------------
54 |
55 | std::string ConfigManager::getConfigDir()
56 | {
57 | // Return cached value if already determined
58 | if (!config_dir_cache_.empty())
59 | {
60 | return config_dir_cache_;
61 | }
62 |
63 | std::vector<std::string> search_paths;
64 |
65 | #ifdef _WIN32
66 | // Windows: %APPDATA%\arceditor
67 | const char *appdata = std::getenv("APPDATA");
68 | if (appdata)
69 | {
70 | search_paths.push_back(std::string(appdata) + "\\arceditor");
71 | }
72 | // Fallback: %USERPROFILE%\.config\arceditor
73 | const char *userprofile = std::getenv("USERPROFILE");
74 | if (userprofile)
75 | {
76 | search_paths.push_back(std::string(userprofile) + "\\.config\\arceditor");
77 | }
78 | #else
79 | // Linux/macOS: XDG_CONFIG_HOME or ~/.config
80 | const char *xdg_config = std::getenv("XDG_CONFIG_HOME");
81 | if (xdg_config)
82 | {
83 | search_paths.push_back(std::string(xdg_config) + "/arceditor");
84 | }
85 |
86 | const char *home = std::getenv("HOME");
87 | if (home)
88 | {
89 | search_paths.push_back(std::string(home) + "/.config/arceditor");
90 | }
91 | #endif
92 |
93 | // Development fallback to use ./config/arceditor
94 | std::string cwd = fs::current_path().string();
95 | std::string dev_config_path = cwd + "/.config/arceditor";
96 |
97 | if (fs::exists(dev_config_path) && fs::is_directory(dev_config_path))
98 | {
99 | // Insert the local development path as the highest priority
100 | search_paths.insert(search_paths.begin(), dev_config_path);
101 | }
102 |
103 | // Find first existing config directory
104 | for (const auto &path : search_paths)
105 | {
106 | if (fs::exists(path) && fs::is_directory(path))
107 | {
108 | config_dir_cache_ = path;
109 | // std::cerr << "Using config directory: " << config_dir_cache_ <<
110 | // std::endl;
111 | return config_dir_cache_;
112 | }
113 | }
114 |
115 | // If no config dir exists, create one in the standard location (first in
116 | // search_paths)
117 | if (!search_paths.empty())
118 | {
119 | std::string target_dir = search_paths[0];
120 | try
121 | {
122 | fs::create_directories(target_dir);
123 | config_dir_cache_ = target_dir;
124 | std::cerr << "Created config directory: " << config_dir_cache_
125 | << std::endl;
126 | return config_dir_cache_;
127 | }
128 | catch (const fs::filesystem_error &e)
129 | {
130 | std::cerr << "Failed to create config directory: " << e.what()
131 | << std::endl;
132 | }
133 | }
134 |
135 | // Last resort: use current directory
136 | config_dir_cache_ = cwd;
137 | std::cerr << "Warning: Using current directory as config dir" << std::endl;
138 | return config_dir_cache_;
139 | }
140 |
141 | std::string ConfigManager::getThemesDir() { return getConfigDir() + "/themes"; }
142 |
143 | std::string ConfigManager::getSyntaxRulesDir()
144 | {
145 | return getConfigDir() + "/syntax_rules";
146 | }
147 |
148 | std::string ConfigManager::getConfigFile()
149 | {
150 | return getConfigDir() + "/config.yaml";
151 | }
152 |
153 | bool ConfigManager::ensureConfigStructure()
154 | {
155 | try
156 | {
157 | std::string config_dir = getConfigDir();
158 |
159 | // Create main config directory (already handled in getConfigDir, but safety
160 | // check)
161 | if (!fs::exists(config_dir))
162 | {
163 | fs::create_directories(config_dir);
164 | }
165 |
166 | // Create subdirectories
167 | std::string themes_dir = config_dir + "/themes";
168 | // std::string syntax_dir = config_dir + "/syntax_rules";
169 |
170 | if (!fs::exists(themes_dir))
171 | {
172 | fs::create_directories(themes_dir);
173 | std::cerr << "Created themes directory: " << themes_dir << std::endl;
174 | }
175 |
176 | // if (!fs::exists(syntax_dir))
177 | // {
178 | // fs::create_directories(syntax_dir);
179 | // std::cerr << "Created syntax_rules directory: " << syntax_dir
180 | // << std::endl;
181 | // }
182 |
183 | // Create default config file if it doesn't exist
184 | std::string config_file = getConfigFile();
185 | if (!fs::exists(config_file))
186 | {
187 | createDefaultConfig(config_file);
188 | }
189 |
190 | return true;
191 | }
192 | catch (const fs::filesystem_error &e)
193 | {
194 | std::cerr << "Error ensuring config structure: " << e.what() << std::endl;
195 | return false;
196 | }
197 | }
198 |
199 | // -----------------------------------------------------------------
200 | // CONFIGURATION (YAML) MANAGEMENT
201 | // -----------------------------------------------------------------
202 |
203 | bool ConfigManager::createDefaultConfig(const std::string &config_file)
204 | {
205 | try
206 | {
207 | YAML::Node config;
208 | config["appearance"]["theme"] = "default";
209 | config["editor"]["tab_size"] = 4;
210 | config["editor"]["line_numbers"] = true;
211 | config["editor"]["cursor_style"] = "auto";
212 | config["syntax"]["highlighting"] = "viewport"; // Changed to string
213 |
214 | std::ofstream file(config_file);
215 | if (!file.is_open())
216 | {
217 | std::cerr << "Failed to create default config file" << std::endl;
218 | return false;
219 | }
220 | file << "# arceditor Configuration File\n";
221 | file << "# This file is automatically generated\n\n";
222 | file << config;
223 | file.close();
224 |
225 | std::cerr << "Created default config: " << config_file << std::endl;
226 | return true;
227 | }
228 | catch (const std::exception &e)
229 | {
230 | std::cerr << "Failed to create config: " << e.what() << std::endl;
231 | return false;
232 | }
233 | }
234 |
235 | bool ConfigManager::loadConfig()
236 | {
237 | std::string config_file = getConfigFile();
238 |
239 | if (!fs::exists(config_file))
240 | {
241 | std::cerr << "Config file not found, using defaults" << std::endl;
242 | return false;
243 | }
244 |
245 | try
246 | {
247 | YAML::Node config = YAML::LoadFile(config_file);
248 |
249 | // Load appearance section
250 | if (config["appearance"] && config["appearance"]["theme"])
251 | {
252 | active_theme_ = config["appearance"]["theme"].as<std::string>();
253 | }
254 |
255 | // Load editor section
256 | if (config["editor"])
257 | {
258 | if (config["editor"]["tab_size"])
259 | {
260 | editor_config_.tab_size = config["editor"]["tab_size"].as<int>();
261 | }
262 | if (config["editor"]["line_numbers"])
263 | {
264 | editor_config_.line_numbers =
265 | config["editor"]["line_numbers"].as<bool>();
266 | }
267 | if (config["editor"]["cursor_style"])
268 | {
269 | editor_config_.cursor_style =
270 | config["editor"]["cursor_style"].as<std::string>();
271 | }
272 | }
273 |
274 | // Load syntax section
275 | if (config["syntax"] && config["syntax"]["highlighting"])
276 | {
277 | std::string mode_str = config["syntax"]["highlighting"].as<std::string>();
278 | syntax_config_.highlighting = parseSyntaxMode(mode_str);
279 | }
280 |
281 | return true;
282 | }
283 | catch (const YAML::Exception &e)
284 | {
285 | std::cerr << "Failed to load config: " << e.what() << std::endl;
286 | return false;
287 | }
288 | }
289 |
290 | bool ConfigManager::saveConfig()
291 | {
292 | std::string config_file = getConfigFile();
293 | YAML::Node config;
294 |
295 | try
296 | {
297 | // Try to load existing config to preserve comments/structure
298 | config = YAML::LoadFile(config_file);
299 | }
300 | catch (const YAML::BadFile &)
301 | {
302 | // File doesn't exist, create new structure
303 | }
304 |
305 | // Update all sections
306 | config["appearance"]["theme"] = active_theme_;
307 | config["editor"]["tab_size"] = editor_config_.tab_size;
308 | config["editor"]["line_numbers"] = editor_config_.line_numbers;
309 | config["editor"]["cursor_style"] = editor_config_.cursor_style;
310 | config["syntax"]["highlighting"] =
311 | syntaxModeToString(syntax_config_.highlighting);
312 |
313 | try
314 | {
315 | std::ofstream file(config_file);
316 | if (!file.is_open())
317 | {
318 | std::cerr << "Failed to open config file for saving" << std::endl;
319 | return false;
320 | }
321 | file << "# arceditor Configuration File\n\n";
322 | file << config;
323 | file.close();
324 | return true;
325 | }
326 | catch (const std::exception &e)
327 | {
328 | std::cerr << "Failed to save config: " << e.what() << std::endl;
329 | return false;
330 | }
331 | }
332 |
333 | // -----------------------------------------------------------------
334 | // LIVE RELOAD IMPLEMENTATION (EFSW)
335 | // -----------------------------------------------------------------
336 |
337 | void ConfigManager::registerReloadCallback(ConfigReloadCallback callback)
338 | {
339 | reload_callbacks_.push_back(callback);
340 | }
341 |
342 | void ConfigManager::handleFileChange()
343 | {
344 | std::cerr << "Config file modified. Attempting hot reload..." << std::endl;
345 |
346 | // 1. Re-read the configuration file
347 | if (!loadConfig())
348 | {
349 | std::cerr << "Failed to hot reload configuration. File may be invalid."
350 | << std::endl;
351 | return;
352 | }
353 |
354 | // 2. Notify all subscribed components
355 | for (const auto &callback : reload_callbacks_)
356 | {
357 | // NOTE: In a multi-threaded app, this should be marshaled to the main
358 | // thread.
359 | callback();
360 | }
361 | reload_pending_.store(true);
362 | // std::cerr << "Configuration hot reload complete." << std::endl;
363 | }
364 |
365 | bool ConfigManager::isReloadPending()
366 | {
367 | // Atomically check the flag and reset it to false in one operation
368 | return reload_pending_.exchange(false);
369 | }
370 |
371 | bool ConfigManager::startWatchingConfig()
372 | {
373 | // 1. Check if watching is already active
374 | if (watcher_instance_ && watcher_listener_)
375 | {
376 | return true; // Already watching
377 | }
378 |
379 | // 2. Instantiate the FileWatcher and Listener
380 | try
381 | {
382 | // The FileWatcher instance is heavy and should be long-lived
383 | watcher_instance_ = std::make_unique<efsw::FileWatcher>();
384 |
385 | // The listener is the custom class we defined earlier
386 | watcher_listener_ = std::make_unique<ConfigFileListener>();
387 |
388 | // 3. Get the directory to watch (the config directory)
389 | std::string configDir = getConfigDir();
390 |
391 | // 4. Add the watch. The 'true' is for recursive watching,
392 | // but the listener only cares about 'config.yaml' anyway.
393 | efsw::WatchID watchID = watcher_instance_->addWatch(
394 | configDir, watcher_listener_.get(), false // false for non-recursive
395 | );
396 |
397 | if (watchID < 0)
398 | {
399 | std::cerr << "Error: EFSW failed to add watch for config directory: "
400 | << configDir << std::endl;
401 | // Cleanup pointers if the watch failed
402 | watcher_instance_.reset();
403 | watcher_listener_.reset();
404 | return false;
405 | }
406 |
407 | // 5. Start the watcher thread
408 | watcher_instance_->watch(); // This starts the background thread
409 |
410 | // std::cerr << "Config watching started for: " << configDir << std::endl;
411 | return true;
412 | }
413 | catch (const std::exception &e)
414 | {
415 | std::cerr << "Fatal Error starting EFSW watcher: " << e.what() << std::endl;
416 | watcher_instance_.reset();
417 | watcher_listener_.reset();
418 | return false;
419 | }
420 | }
421 |
422 | // -----------------------------------------------------------------
423 | // THEME AND SYNTAX MANAGEMENT
424 | // -----------------------------------------------------------------
425 |
426 | std::string ConfigManager::getThemeFile(const std::string &theme_name)
427 | {
428 | std::string themes_dir = getThemesDir();
429 | std::string theme_file = themes_dir + "/" + theme_name + ".theme";
430 |
431 | if (fs::exists(theme_file))
432 | {
433 | return theme_file;
434 | }
435 |
436 | // Fallback: Check project's "themes" directory for default files
437 | std::string dev_theme = "themes/" + theme_name + ".theme";
438 | if (fs::exists(dev_theme))
439 | {
440 | return dev_theme;
441 | }
442 |
443 | std::cerr << "Theme file not found: " << theme_name << std::endl;
444 | return "";
445 | }
446 |
447 | std::string ConfigManager::getSyntaxFile(const std::string &language)
448 | {
449 | std::string syntax_dir = getSyntaxRulesDir();
450 | std::string syntax_file = syntax_dir + "/" + language + ".yaml";
451 |
452 | if (fs::exists(syntax_file))
453 | {
454 | return syntax_file;
455 | }
456 |
457 | // Fallback: Check project's "syntax_rules" directory for default files
458 | std::string dev_syntax = "treesitter/" + language + ".yaml";
459 | if (fs::exists(dev_syntax))
460 | {
461 | return dev_syntax;
462 | }
463 |
464 | return ""; // Not found
465 | }
466 |
467 | std::string ConfigManager::getActiveTheme() { return active_theme_; }
468 |
469 | bool ConfigManager::setActiveTheme(const std::string &theme_name)
470 | {
471 | // Verify theme exists
472 | std::string theme_file = getThemeFile(theme_name);
473 | if (theme_file.empty())
474 | {
475 | std::cerr << "Cannot set theme, file not found: " << theme_name
476 | << std::endl;
477 | return false;
478 | }
479 |
480 | active_theme_ = theme_name;
481 | saveConfig(); // Persist the change
482 | return true;
483 | }
484 |
485 | bool ConfigManager::copyProjectFilesToConfig()
486 | {
487 | try
488 | {
489 | std::string config_dir = getConfigDir();
490 |
491 | // Copy themes
492 | if (fs::exists("themes") && fs::is_directory("themes"))
493 | {
494 | std::string target_themes = config_dir + "/themes";
495 | fs::create_directories(target_themes);
496 |
497 | for (const auto &entry : fs::directory_iterator("themes"))
498 | {
499 | if (entry.is_regular_file() && entry.path().extension() == ".theme")
500 | {
501 | std::string filename = entry.path().filename().string();
502 | std::string target = target_themes + "/" + filename;
503 |
504 | // Only copy if doesn't exist (don't overwrite user themes)
505 | if (!fs::exists(target))
506 | {
507 | fs::copy_file(entry.path(), target);
508 | std::cerr << "Copied theme: " << filename << std::endl;
509 | }
510 | }
511 | }
512 | }
513 |
514 | // Copy syntax rules
515 | if (fs::exists("syntax_rules") && fs::is_directory("syntax_rules"))
516 | {
517 | std::string target_syntax = config_dir + "/syntax_rules";
518 | fs::create_directories(target_syntax);
519 |
520 | for (const auto &entry : fs::directory_iterator("syntax_rules"))
521 | {
522 | if (entry.is_regular_file() && entry.path().extension() == ".yaml")
523 | {
524 | std::string filename = entry.path().filename().string();
525 | std::string target = target_syntax + "/" + filename;
526 |
527 | // Only copy if doesn't exist
528 | if (!fs::exists(target))
529 | {
530 | fs::copy_file(entry.path(), target);
531 | std::cerr << "Copied syntax rule: " << filename << std::endl;
532 | }
533 | }
534 | }
535 | }
536 |
537 | return true;
538 | }
539 | catch (const fs::filesystem_error &e)
540 | {
541 | std::cerr << "Error copying project files: " << e.what() << std::endl;
542 | return false;
543 | }
544 | }
545 |
546 | SyntaxMode ConfigManager::parseSyntaxMode(const std::string &mode_str)
547 | {
548 | std::string lower = mode_str;
549 | std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
550 |
551 | if (lower == "none" || lower == "false" || lower == "off")
552 | return SyntaxMode::NONE;
553 | else if (lower == "viewport" || lower == "lazy" || lower == "dynamic")
554 | return SyntaxMode::VIEWPORT;
555 | else if (lower == "full" || lower == "immediate" || lower == "true")
556 | return SyntaxMode::FULL;
557 |
558 | std::cerr << "Unknown syntax mode '" << mode_str << "', using viewport"
559 | << std::endl;
560 | return SyntaxMode::VIEWPORT;
561 | }
562 |
563 | std::string ConfigManager::syntaxModeToString(SyntaxMode mode)
564 | {
565 | switch (mode)
566 | {
567 | case SyntaxMode::NONE:
568 | return "none";
569 | case SyntaxMode::VIEWPORT:
570 | return "viewport";
571 | case SyntaxMode::FULL:
572 | return "full";
573 | default:
574 | return "viewport";
575 | }
576 | }
577 |
578 | // NEW: Setters
579 | void ConfigManager::setTabSize(int size)
580 | {
581 | if (size < 1)
582 | size = 1;
583 | if (size > 16)
584 | size = 16;
585 | editor_config_.tab_size = size;
586 | saveConfig();
587 | }
588 |
589 | void ConfigManager::setLineNumbers(bool enabled)
590 | {
591 | editor_config_.line_numbers = enabled;
592 | saveConfig();
593 | }
594 |
595 | void ConfigManager::setCursorStyle(const std::string &style)
596 | {
597 | editor_config_.cursor_style = style;
598 | saveConfig();
599 | }
600 |
601 | void ConfigManager::setSyntaxMode(SyntaxMode mode)
602 | {
603 | syntax_config_.highlighting = mode;
604 | saveConfig();
605 | }
```
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
```
1 | cmake_minimum_required(VERSION 3.16)
2 | project(arc VERSION 0.0.1 LANGUAGES CXX C)
3 |
4 | set(CMAKE_CXX_STANDARD 20)
5 | set(CMAKE_CXX_STANDARD_REQUIRED ON)
6 |
7 | # Define a central location for external dependencies
8 | set(DEPS_DIR ${CMAKE_SOURCE_DIR}/deps)
9 |
10 | # Initialize Tree-sitter as enabled by default
11 | set(TREE_SITTER_ENABLED TRUE)
12 |
13 | # Debug Delta
14 | # add_compile_definitions(DEBUG_DELTA_UNDO)
15 |
16 | # ----------------------------------------------------
17 | # 1. Tree-sitter Core Library
18 | # ----------------------------------------------------
19 |
20 | # Manually list core source files
21 | set(TS_CORE_SOURCES
22 | ${DEPS_DIR}/tree-sitter-core/lib/src/language.c
23 | ${DEPS_DIR}/tree-sitter-core/lib/src/lexer.c
24 | ${DEPS_DIR}/tree-sitter-core/lib/src/node.c
25 | ${DEPS_DIR}/tree-sitter-core/lib/src/parser.c
26 | ${DEPS_DIR}/tree-sitter-core/lib/src/query.c
27 | ${DEPS_DIR}/tree-sitter-core/lib/src/tree.c
28 | ${DEPS_DIR}/tree-sitter-core/lib/src/tree_cursor.c
29 | ${DEPS_DIR}/tree-sitter-core/lib/src/alloc.c
30 | ${DEPS_DIR}/tree-sitter-core/lib/src/get_changed_ranges.c
31 | ${DEPS_DIR}/tree-sitter-core/lib/src/stack.c
32 | ${DEPS_DIR}/tree-sitter-core/lib/src/subtree.c
33 | ${DEPS_DIR}/tree-sitter-core/lib/src/point.c
34 | ${DEPS_DIR}/tree-sitter-core/lib/src/wasm_store.c
35 | )
36 |
37 | # Check if Tree-sitter headers exist
38 | if(NOT EXISTS ${DEPS_DIR}/tree-sitter-core/lib/include/tree_sitter/api.h)
39 | message(WARNING "Tree-sitter header files not found at ${DEPS_DIR}/tree-sitter-core/lib/include/tree_sitter/api.h")
40 | set(TREE_SITTER_ENABLED FALSE)
41 | endif()
42 |
43 | if(TREE_SITTER_ENABLED)
44 | # Verify core files exist
45 | set(MISSING_FILES "")
46 | foreach(file ${TS_CORE_SOURCES})
47 | if(NOT EXISTS ${file})
48 | list(APPEND MISSING_FILES ${file})
49 | endif()
50 | endforeach()
51 |
52 | if(MISSING_FILES)
53 | message(WARNING "Missing Tree-sitter core files: ${MISSING_FILES}")
54 | set(TREE_SITTER_ENABLED FALSE)
55 | else()
56 | message(STATUS "Tree-sitter core sources detected: ${TS_CORE_SOURCES}")
57 | message(STATUS "Building Tree-sitter core library.")
58 |
59 | add_library(tree-sitter-core STATIC ${TS_CORE_SOURCES})
60 |
61 | # Explicitly set these as C sources
62 | set_source_files_properties(${TS_CORE_SOURCES} PROPERTIES LANGUAGE C)
63 |
64 | # The public API headers are in lib/include/
65 | target_include_directories(tree-sitter-core PUBLIC
66 | ${DEPS_DIR}/tree-sitter-core/lib/include
67 | )
68 | # Explicitly set C standard for C files
69 | target_compile_features(tree-sitter-core PUBLIC c_std_99)
70 |
71 | # Force static runtime for Tree-sitter to match main executable
72 | if(MSVC)
73 | target_compile_options(tree-sitter-core PRIVATE
74 | $<$<CONFIG:Debug>:/MTd>
75 | $<$<CONFIG:Release>:/MT>
76 | )
77 | endif()
78 |
79 | set(TS_LIBRARIES tree-sitter-core)
80 | set(TS_INCLUDES ${DEPS_DIR}/tree-sitter-core/lib/include)
81 |
82 | message(STATUS "Tree-sitter enabled with include path: ${TS_INCLUDES}")
83 | endif()
84 | else()
85 | message(WARNING "Tree-sitter core sources or headers not found. Disabling Tree-sitter features.")
86 | set(TREE_SITTER_ENABLED FALSE)
87 | set(TS_LIBRARIES "")
88 | set(TS_INCLUDES "")
89 | endif()
90 |
91 | # ----------------------------------------------------
92 | # 2. Language Parsers (Auto-Discovery) - FIXED FOR WINDOWS
93 | # ----------------------------------------------------
94 |
95 | if(TREE_SITTER_ENABLED)
96 | message(STATUS "=== Tree-sitter Auto-Discovery ===")
97 |
98 | # Define parsers - use CMake lists instead of colon-separated strings
99 | # Format: list of pairs (lang_name, parser_path)
100 | set(PARSER_NAMES
101 | "python" "c" "cpp" "rust" "markdown" "javascript" "typescript" "tsx" "zig" "go"
102 | )
103 | set(PARSER_PATHS
104 | "${DEPS_DIR}/tree-sitter-python"
105 | "${DEPS_DIR}/tree-sitter-c"
106 | "${DEPS_DIR}/tree-sitter-cpp"
107 | "${DEPS_DIR}/tree-sitter-rust"
108 | "${DEPS_DIR}/tree-sitter-markdown/tree-sitter-markdown"
109 | "${DEPS_DIR}/tree-sitter-javascript"
110 | "${DEPS_DIR}/tree-sitter-typescript/typescript"
111 | "${DEPS_DIR}/tree-sitter-typescript/tsx"
112 | "${DEPS_DIR}/tree-sitter-zig"
113 | "${DEPS_DIR}/tree-sitter-go"
114 | )
115 |
116 | set(DISCOVERED_PARSERS "")
117 |
118 | # Iterate using indices
119 | list(LENGTH PARSER_NAMES parser_count)
120 | math(EXPR parser_count "${parser_count} - 1")
121 |
122 | foreach(i RANGE ${parser_count})
123 | list(GET PARSER_NAMES ${i} lang_name)
124 | list(GET PARSER_PATHS ${i} parser_dir)
125 |
126 | if(NOT EXISTS ${parser_dir})
127 | message(STATUS " ✗ Skipping ${lang_name}: directory not found at ${parser_dir}")
128 | continue()
129 | endif()
130 |
131 | # Collect source files
132 | set(PARSER_SOURCES "")
133 |
134 | # Check for parser.c (required)
135 | if(EXISTS ${parser_dir}/src/parser.c)
136 | list(APPEND PARSER_SOURCES ${parser_dir}/src/parser.c)
137 | else()
138 | message(STATUS " ✗ Skipping ${lang_name}: no parser.c at ${parser_dir}/src/")
139 | continue()
140 | endif()
141 |
142 | # Check for scanner files (optional)
143 | if(EXISTS ${parser_dir}/src/scanner.c)
144 | list(APPEND PARSER_SOURCES ${parser_dir}/src/scanner.c)
145 | endif()
146 |
147 | if(EXISTS ${parser_dir}/src/scanner.cc)
148 | list(APPEND PARSER_SOURCES ${parser_dir}/src/scanner.cc)
149 | endif()
150 |
151 | # Create library
152 | add_library(tree-sitter-${lang_name} STATIC ${PARSER_SOURCES})
153 |
154 | # Set as C sources
155 | set_source_files_properties(${PARSER_SOURCES} PROPERTIES LANGUAGE C)
156 |
157 | # Set C99 standard
158 | target_compile_features(tree-sitter-${lang_name} PUBLIC c_std_99)
159 |
160 | # Force static runtime for parsers to match main executable
161 | if(MSVC)
162 | target_compile_options(tree-sitter-${lang_name} PRIVATE
163 | $<$<CONFIG:Debug>:/MTd>
164 | $<$<CONFIG:Release>:/MT>
165 | )
166 | endif()
167 |
168 | # Include directories for scanner files
169 | target_include_directories(tree-sitter-${lang_name} PRIVATE
170 | ${parser_dir}/src
171 | )
172 |
173 | # Add to libraries list
174 | list(APPEND TS_LIBRARIES tree-sitter-${lang_name})
175 | list(APPEND DISCOVERED_PARSERS ${lang_name})
176 |
177 | message(STATUS " ✓ Built parser: ${lang_name}")
178 | endforeach()
179 |
180 | # ----------------------------------------------------
181 | # 3. Generate Language Registry Header (Auto-registration)
182 | # ----------------------------------------------------
183 |
184 | if(DISCOVERED_PARSERS)
185 | set(LANG_REGISTRY_FILE "${CMAKE_BINARY_DIR}/generated/language_registry.h")
186 | file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/generated")
187 |
188 | # Build the header file content
189 | set(REGISTRY_CONTENT "// Auto-generated by CMake - DO NOT EDIT MANUALLY\n")
190 | string(APPEND REGISTRY_CONTENT "// Generated from: ${CMAKE_CURRENT_LIST_FILE}\n\n")
191 | string(APPEND REGISTRY_CONTENT "#pragma once\n\n")
192 | string(APPEND REGISTRY_CONTENT "#ifdef TREE_SITTER_ENABLED\n\n")
193 | string(APPEND REGISTRY_CONTENT "#include <tree_sitter/api.h>\n")
194 | string(APPEND REGISTRY_CONTENT "#include <unordered_map>\n")
195 | string(APPEND REGISTRY_CONTENT "#include <string>\n\n")
196 |
197 | # Extern declarations for all discovered languages
198 | string(APPEND REGISTRY_CONTENT "// External language function declarations\n")
199 | string(APPEND REGISTRY_CONTENT "extern \"C\" {\n")
200 | foreach(lang ${DISCOVERED_PARSERS})
201 | string(APPEND REGISTRY_CONTENT " const TSLanguage *tree_sitter_${lang}();\n")
202 | endforeach()
203 | string(APPEND REGISTRY_CONTENT "}\n\n")
204 |
205 | # Registration function
206 | string(APPEND REGISTRY_CONTENT "// Auto-register all available languages\n")
207 | string(APPEND REGISTRY_CONTENT "inline void registerAllLanguages(std::unordered_map<std::string, const TSLanguage* (*)()>& registry) {\n")
208 | foreach(lang ${DISCOVERED_PARSERS})
209 | string(APPEND REGISTRY_CONTENT " registry[\"${lang}\"] = tree_sitter_${lang};\n")
210 | endforeach()
211 | string(APPEND REGISTRY_CONTENT "}\n\n")
212 |
213 | # List of available languages as a comment
214 | string(APPEND REGISTRY_CONTENT "// Available languages: ")
215 | string(JOIN DISCOVERED_PARSERS ", " LANG_LIST)
216 | string(APPEND REGISTRY_CONTENT "${LANG_LIST}\n\n")
217 |
218 | string(APPEND REGISTRY_CONTENT "#endif // TREE_SITTER_ENABLED\n")
219 |
220 | # Write the file
221 | file(WRITE ${LANG_REGISTRY_FILE} "${REGISTRY_CONTENT}")
222 |
223 | message(STATUS "Generated language registry: ${LANG_REGISTRY_FILE}")
224 | message(STATUS " Registered parsers: ${DISCOVERED_PARSERS}")
225 | else()
226 | message(WARNING "No parsers discovered, skipping registry generation")
227 | set(TREE_SITTER_ENABLED FALSE)
228 | endif()
229 |
230 | message(STATUS "=== End Tree-sitter Auto-Discovery ===")
231 |
232 | endif()
233 |
234 | if(NOT TREE_SITTER_ENABLED)
235 | message(STATUS "Tree-sitter disabled - using fallback syntax highlighting")
236 | set(TS_LIBRARIES "")
237 | set(TS_INCLUDES "")
238 | endif()
239 |
240 | # ----------------------------------------------------
241 | # 4. EFSW (Event File System Watcher) - For Live Reloading
242 | # ----------------------------------------------------
243 | set(EFSW_BASE_DIR ${DEPS_DIR}/efsw)
244 | set(EFSW_SOURCES "")
245 |
246 | # List ALL required core files from the flattened structure (src/efsw/)
247 | list(APPEND EFSW_SOURCES
248 | # Core Files (Unconditional)
249 | ${EFSW_BASE_DIR}/src/efsw/Debug.cpp
250 | ${EFSW_BASE_DIR}/src/efsw/DirWatcherGeneric.cpp
251 | ${EFSW_BASE_DIR}/src/efsw/DirectorySnapshot.cpp
252 | ${EFSW_BASE_DIR}/src/efsw/DirectorySnapshotDiff.cpp
253 | ${EFSW_BASE_DIR}/src/efsw/FileInfo.cpp
254 | ${EFSW_BASE_DIR}/src/efsw/FileSystem.cpp
255 | ${EFSW_BASE_DIR}/src/efsw/FileWatcher.cpp
256 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherCWrapper.cpp
257 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherImpl.cpp
258 | ${EFSW_BASE_DIR}/src/efsw/Log.cpp
259 | ${EFSW_BASE_DIR}/src/efsw/String.cpp
260 | ${EFSW_BASE_DIR}/src/efsw/System.cpp
261 | ${EFSW_BASE_DIR}/src/efsw/Watcher.cpp
262 |
263 | # CRITICAL FIX: Add Generic implementation for default constructors
264 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherGeneric.cpp
265 | ${EFSW_BASE_DIR}/src/efsw/WatcherGeneric.cpp
266 | )
267 |
268 | # Conditionally add the correct platform backend
269 | if(CMAKE_SYSTEM_NAME MATCHES "Linux")
270 | list(APPEND EFSW_SOURCES
271 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherInotify.cpp
272 | ${EFSW_BASE_DIR}/src/efsw/WatcherInotify.cpp
273 | ${EFSW_BASE_DIR}/src/efsw/platform/posix/FileSystemImpl.cpp
274 | ${EFSW_BASE_DIR}/src/efsw/platform/posix/SystemImpl.cpp
275 | )
276 | elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") # macOS
277 | list(APPEND EFSW_SOURCES
278 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherFSEvents.cpp
279 | ${EFSW_BASE_DIR}/src/efsw/WatcherFSEvents.cpp
280 | ${EFSW_BASE_DIR}/src/efsw/platform/posix/FileSystemImpl.cpp
281 | ${EFSW_BASE_DIR}/src/efsw/platform/posix/SystemImpl.cpp
282 | )
283 | elseif(WIN32)
284 | list(APPEND EFSW_SOURCES
285 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherWin32.cpp
286 | ${EFSW_BASE_DIR}/src/efsw/WatcherWin32.cpp
287 | ${EFSW_BASE_DIR}/src/efsw/platform/win/FileSystemImpl.cpp
288 | ${EFSW_BASE_DIR}/src/efsw/platform/win/SystemImpl.cpp
289 | )
290 | else()
291 | list(APPEND EFSW_SOURCES
292 | ${EFSW_BASE_DIR}/src/efsw/FileWatcherGeneric.cpp
293 | ${EFSW_BASE_DIR}/src/efsw/WatcherGeneric.cpp
294 | )
295 | endif()
296 |
297 | if(EFSW_SOURCES)
298 | add_library(efsw STATIC ${EFSW_SOURCES})
299 |
300 | target_include_directories(efsw PUBLIC
301 | ${EFSW_BASE_DIR}/include
302 | ${EFSW_BASE_DIR}/src
303 | )
304 |
305 | # Force static runtime for EFSW to match main executable
306 | if(MSVC)
307 | target_compile_options(efsw PRIVATE
308 | $<$<CONFIG:Debug>:/MTd>
309 | $<$<CONFIG:Release>:/MT>
310 | )
311 | endif()
312 |
313 | find_package(Threads REQUIRED)
314 | target_link_libraries(efsw PRIVATE Threads::Threads)
315 |
316 | if(CMAKE_SYSTEM_NAME MATCHES "Linux")
317 | target_link_libraries(efsw PRIVATE rt)
318 | elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin")
319 | target_link_libraries(efsw PRIVATE CoreServices)
320 | endif()
321 |
322 | message(STATUS "Added EFSW from deps folder.")
323 | set(EFSW_LIBRARIES efsw)
324 | else()
325 | message(WARNING "EFSW sources missing. Live reload feature disabled.")
326 | set(EFSW_LIBRARIES "")
327 | endif()
328 |
329 | # ----------------------------------------------------
330 | # 5. Platform-Specific Settings
331 | # ----------------------------------------------------
332 |
333 | # Windows-specific optimizations
334 | if(WIN32)
335 | set(VCPKG_APPLOCAL_DEPS OFF)
336 | set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF)
337 | set(CMAKE_COLOR_MAKEFILE OFF)
338 |
339 | if(MINGW)
340 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++ -static")
341 | endif()
342 |
343 | if(MSVC)
344 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
345 | endif()
346 | endif()
347 |
348 | # Find packages - platform-specific approach
349 | if(WIN32)
350 | set(PDCURSESMOD_DIR ${DEPS_DIR}/PDCursesMod/wincon)
351 |
352 | if(EXISTS "${PDCURSESMOD_DIR}/pdcurses.lib")
353 | message(STATUS "Using locally built PDCursesMod from ${PDCURSESMOD_DIR}")
354 |
355 | add_library(pdcursesmod STATIC IMPORTED)
356 | set_target_properties(pdcursesmod PROPERTIES
357 | IMPORTED_LOCATION "${PDCURSESMOD_DIR}/pdcurses.lib"
358 | INTERFACE_INCLUDE_DIRECTORIES "${DEPS_DIR}/PDCursesMod"
359 | )
360 |
361 | set(CURSES_LIBRARIES pdcursesmod)
362 | set(CURSES_INCLUDE_DIRS "${DEPS_DIR}/PDCursesMod")
363 | else()
364 | message(FATAL_ERROR "PDCursesMod not found at ${PDCURSESMOD_DIR}. Please build it manually with nmake -f Makefile.vc HAVE_VT=Y")
365 | endif()
366 |
367 | elseif(ANDROID)
368 | find_package(PkgConfig REQUIRED)
369 | pkg_check_modules(NCURSES REQUIRED ncurses)
370 | set(CURSES_LIBRARIES ${NCURSES_LIBRARIES})
371 | set(CURSES_INCLUDE_DIRS ${NCURSES_INCLUDE_DIRS})
372 |
373 | else()
374 | find_package(Curses REQUIRED)
375 | set(CURSES_LIBRARIES ${CURSES_LIBRARIES})
376 | set(CURSES_INCLUDE_DIRS ${CURSES_INCLUDE_DIR})
377 | endif()
378 |
379 | find_package(yaml-cpp CONFIG REQUIRED)
380 |
381 | # ----------------------------------------------------
382 | # 6. Main Executable
383 | # ----------------------------------------------------
384 |
385 | # Source files
386 | set(SOURCES
387 | src/main.cpp
388 | src/core/editor.cpp
389 | src/core/buffer.cpp
390 | src/core/config_manager.cpp
391 | src/ui/input_handler.cpp
392 | # src/ui/renderer.cpp
393 | src/ui/style_manager.cpp
394 | src/features/syntax_config_loader.cpp
395 | src/features/syntax_highlighter.cpp
396 | )
397 |
398 | # Create executable
399 | add_executable(arc ${SOURCES})
400 |
401 | # Conditionally add Tree-sitter compile definition
402 | if(TREE_SITTER_ENABLED)
403 | target_compile_definitions(arc PRIVATE TREE_SITTER_ENABLED)
404 | message(STATUS "Compiling with Tree-sitter support enabled")
405 | else()
406 | message(STATUS "Compiling without Tree-sitter support")
407 | endif()
408 |
409 | # Link libraries
410 | target_link_libraries(arc PRIVATE
411 | yaml-cpp::yaml-cpp
412 | ${CURSES_LIBRARIES}
413 | ${TS_LIBRARIES}
414 | ${EFSW_LIBRARIES}
415 | )
416 |
417 | if(WIN32)
418 | # Link the Windows Multimedia library required by PDCursesMod's beep()
419 | target_link_libraries(arc PRIVATE winmm)
420 | endif()
421 |
422 | # Include directories
423 | target_include_directories(arc PRIVATE
424 | .
425 | ${CURSES_INCLUDE_DIRS}
426 | ${TS_INCLUDES}
427 | )
428 |
429 | # Add generated headers directory if Tree-sitter is enabled
430 | if(TREE_SITTER_ENABLED)
431 | target_include_directories(arc PRIVATE ${CMAKE_BINARY_DIR}/generated)
432 | endif()
433 |
434 | # Compiler flags with optimizations
435 | if(MSVC)
436 | target_compile_options(arc PRIVATE /W4 /MP)
437 | target_compile_options(arc PRIVATE $<$<CONFIG:Debug>:/MTd> $<$<CONFIG:Release>:/MT>)
438 | else()
439 | target_compile_options(arc PRIVATE -Wall -Wextra)
440 |
441 | if(ANDROID)
442 | target_link_libraries(arc PRIVATE ${NCURSES_LINK_LIBRARIES})
443 | endif()
444 |
445 | if(CMAKE_BUILD_TYPE STREQUAL "Debug")
446 | target_compile_options(arc PRIVATE -g1 -O0 -fno-omit-frame-pointer)
447 | else()
448 | target_compile_options(arc PRIVATE -O2)
449 | endif()
450 | endif()
451 |
452 | # ----------------------------------------------------
453 | # 7. Build Summary
454 | # ----------------------------------------------------
455 |
456 | message(STATUS "")
457 | message(STATUS "========================================")
458 | message(STATUS "Arc Editor Build Configuration")
459 | message(STATUS "========================================")
460 | message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
461 | message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}")
462 | message(STATUS "Platform: ${CMAKE_SYSTEM_NAME}")
463 | message(STATUS "Curses library: ${CURSES_LIBRARIES}")
464 | message(STATUS "Tree-sitter enabled: ${TREE_SITTER_ENABLED}")
465 | if(TREE_SITTER_ENABLED)
466 | message(STATUS " Tree-sitter libraries: ${TS_LIBRARIES}")
467 | message(STATUS " Tree-sitter includes: ${TS_INCLUDES}")
468 | message(STATUS " Discovered parsers: ${DISCOVERED_PARSERS}")
469 | endif()
470 | message(STATUS "EFSW enabled: ${EFSW_LIBRARIES}")
471 | if(WIN32)
472 | message(STATUS "VCPKG_APPLOCAL_DEPS: ${VCPKG_APPLOCAL_DEPS}")
473 | endif()
474 | message(STATUS "========================================")
475 | message(STATUS "")
476 |
```
--------------------------------------------------------------------------------
/deps/tree-sitter-markdown/bindings/rust/parser.rs:
--------------------------------------------------------------------------------
```rust
1 | use std::collections::HashMap;
2 | use std::num::NonZeroU16;
3 |
4 | use tree_sitter::{InputEdit, Language, Node, Parser, Point, Range, Tree, TreeCursor};
5 |
6 | use crate::{INLINE_LANGUAGE, LANGUAGE};
7 |
8 | /// A parser that produces [`MarkdownTree`]s.
9 | ///
10 | /// This is a convenience wrapper around [`LANGUAGE`] and [`INLINE_LANGUAGE`].
11 | pub struct MarkdownParser {
12 | parser: Parser,
13 | block_language: Language,
14 | inline_language: Language,
15 | }
16 |
17 | /// A stateful object for walking a [`MarkdownTree`] efficiently.
18 | ///
19 | /// This exposes the same methdos as [`TreeCursor`], but abstracts away the
20 | /// double block / inline structure of [`MarkdownTree`].
21 | pub struct MarkdownCursor<'a> {
22 | markdown_tree: &'a MarkdownTree,
23 | block_cursor: TreeCursor<'a>,
24 | inline_cursor: Option<TreeCursor<'a>>,
25 | }
26 |
27 | impl<'a> MarkdownCursor<'a> {
28 | /// Get the cursor's current [`Node`].
29 | pub fn node(&self) -> Node<'a> {
30 | match &self.inline_cursor {
31 | Some(cursor) => cursor.node(),
32 | None => self.block_cursor.node(),
33 | }
34 | }
35 |
36 | /// Returns `true` if the current node is from the (inline language)[INLINE_LANGUAGE]
37 | ///
38 | /// This information is needed to handle "tree-sitter internal" data like
39 | /// [`field_id`](Self::field_id) correctly.
40 | pub fn is_inline(&self) -> bool {
41 | self.inline_cursor.is_some()
42 | }
43 |
44 | /// Get the numerical field id of this tree cursor’s current node.
45 | ///
46 | /// You will need to call [`is_inline`](Self::is_inline) to find out if the
47 | /// current node is an inline or block node.
48 | ///
49 | /// See also [`field_name`](Self::field_name).
50 | pub fn field_id(&self) -> Option<NonZeroU16> {
51 | match &self.inline_cursor {
52 | Some(cursor) => cursor.field_id(),
53 | None => self.block_cursor.field_id(),
54 | }
55 | }
56 |
57 | /// Get the field name of this tree cursor’s current node.
58 | ///
59 | /// You will need to call [`is_inline`](Self::is_inline) to find out if the
60 | /// current node is an inline or block node.
61 | pub fn field_name(&self) -> Option<&'static str> {
62 | match &self.inline_cursor {
63 | Some(cursor) => cursor.field_name(),
64 | None => self.block_cursor.field_name(),
65 | }
66 | }
67 |
68 | fn move_to_inline_tree(&mut self) -> bool {
69 | let node = self.block_cursor.node();
70 | match node.kind() {
71 | "inline" | "pipe_table_cell" => {
72 | if let Some(inline_tree) = self.markdown_tree.inline_tree(&node) {
73 | self.inline_cursor = Some(inline_tree.walk());
74 | return true;
75 | }
76 | }
77 | _ => (),
78 | }
79 | false
80 | }
81 |
82 | fn move_to_block_tree(&mut self) {
83 | self.inline_cursor = None;
84 | }
85 |
86 | /// Move this cursor to the first child of its current node.
87 | ///
88 | /// This returns `true` if the cursor successfully moved, and returns `false` if there were no
89 | /// children.
90 | /// If the cursor is currently at a node in the block tree and it has an associated inline tree, it
91 | /// will descend into the inline tree.
92 | pub fn goto_first_child(&mut self) -> bool {
93 | match &mut self.inline_cursor {
94 | Some(cursor) => cursor.goto_first_child(),
95 | None => {
96 | if self.move_to_inline_tree() {
97 | if !self.inline_cursor.as_mut().unwrap().goto_first_child() {
98 | self.move_to_block_tree();
99 | false
100 | } else {
101 | true
102 | }
103 | } else {
104 | self.block_cursor.goto_first_child()
105 | }
106 | }
107 | }
108 | }
109 |
110 | /// Move this cursor to the parent of its current node.
111 | ///
112 | /// This returns true if the cursor successfully moved, and returns false if there was no
113 | /// parent node (the cursor was already on the root node).
114 | /// If the cursor moves to the root node of an inline tree, the it ascents to the associated
115 | /// node in the block tree.
116 | pub fn goto_parent(&mut self) -> bool {
117 | match &mut self.inline_cursor {
118 | Some(inline_cursor) => {
119 | inline_cursor.goto_parent();
120 | if inline_cursor.node().parent().is_none() {
121 | self.move_to_block_tree();
122 | }
123 | true
124 | }
125 | None => self.block_cursor.goto_parent(),
126 | }
127 | }
128 |
129 | /// Move this cursor to the next sibling of its current node.
130 | ///
131 | /// This returns true if the cursor successfully moved, and returns false if there was no next
132 | /// sibling node.
133 | pub fn goto_next_sibling(&mut self) -> bool {
134 | match &mut self.inline_cursor {
135 | Some(inline_cursor) => inline_cursor.goto_next_sibling(),
136 | None => self.block_cursor.goto_next_sibling(),
137 | }
138 | }
139 |
140 | /// Move this cursor to the first child of its current node that extends beyond the given byte offset.
141 | ///
142 | /// This returns the index of the child node if one was found, and returns None if no such child was found.
143 | /// If the cursor is currently at a node in the block tree and it has an associated inline tree, it
144 | /// will descend into the inline tree.
145 | pub fn goto_first_child_for_byte(&mut self, index: usize) -> Option<usize> {
146 | match &mut self.inline_cursor {
147 | Some(cursor) => cursor.goto_first_child_for_byte(index),
148 | None => {
149 | if self.move_to_inline_tree() {
150 | self.inline_cursor
151 | .as_mut()
152 | .unwrap()
153 | .goto_first_child_for_byte(index)
154 | } else {
155 | self.block_cursor.goto_first_child_for_byte(index)
156 | }
157 | }
158 | }
159 | }
160 |
161 | /// Move this cursor to the first child of its current node that extends beyond the given point.
162 | ///
163 | /// This returns the index of the child node if one was found, and returns None if no such child was found.
164 | /// If the cursor is currently at a node in the block tree and it has an associated inline tree, it
165 | /// will descend into the inline tree.
166 | pub fn goto_first_child_for_point(&mut self, index: Point) -> Option<usize> {
167 | match &mut self.inline_cursor {
168 | Some(cursor) => cursor.goto_first_child_for_point(index),
169 | None => {
170 | if self.move_to_inline_tree() {
171 | self.inline_cursor
172 | .as_mut()
173 | .unwrap()
174 | .goto_first_child_for_point(index)
175 | } else {
176 | self.block_cursor.goto_first_child_for_point(index)
177 | }
178 | }
179 | }
180 | }
181 | }
182 |
183 | /// An object that holds a combined markdown tree.
184 | #[derive(Debug, Clone)]
185 | pub struct MarkdownTree {
186 | block_tree: Tree,
187 | inline_trees: Vec<Tree>,
188 | inline_indices: HashMap<usize, usize>,
189 | }
190 |
191 | impl MarkdownTree {
192 | /// Edit the block tree and inline trees to keep them in sync with source code that has been
193 | /// edited.
194 | ///
195 | /// You must describe the edit both in terms of byte offsets and in terms of
196 | /// row/column coordinates.
197 | pub fn edit(&mut self, edit: &InputEdit) {
198 | self.block_tree.edit(edit);
199 | for inline_tree in self.inline_trees.iter_mut() {
200 | inline_tree.edit(edit);
201 | }
202 | }
203 |
204 | /// Returns the block tree for the parsed document
205 | pub fn block_tree(&self) -> &Tree {
206 | &self.block_tree
207 | }
208 |
209 | /// Returns the inline tree for the given inline node.
210 | ///
211 | /// Returns `None` if the given node does not have an associated inline tree. Either because
212 | /// the nodes type is not `inline` or because the inline content is empty.
213 | pub fn inline_tree(&self, parent: &Node) -> Option<&Tree> {
214 | let index = *self.inline_indices.get(&parent.id())?;
215 | Some(&self.inline_trees[index])
216 | }
217 |
218 | /// Returns the list of all inline trees
219 | pub fn inline_trees(&self) -> &[Tree] {
220 | &self.inline_trees
221 | }
222 |
223 | /// Create a new [`MarkdownCursor`] starting from the root of the tree.
224 | pub fn walk(&self) -> MarkdownCursor {
225 | MarkdownCursor {
226 | markdown_tree: self,
227 | block_cursor: self.block_tree.walk(),
228 | inline_cursor: None,
229 | }
230 | }
231 | }
232 |
233 | impl Default for MarkdownParser {
234 | fn default() -> Self {
235 | let block_language = LANGUAGE.into();
236 | let inline_language = INLINE_LANGUAGE.into();
237 | let parser = Parser::new();
238 | MarkdownParser {
239 | parser,
240 | block_language,
241 | inline_language,
242 | }
243 | }
244 | }
245 |
246 | impl MarkdownParser {
247 | /// Parse a slice of UTF8 text.
248 | ///
249 | /// # Arguments:
250 | /// * `text` The UTF8-encoded text to parse.
251 | /// * `old_tree` A previous syntax tree parsed from the same document.
252 | /// If the text of the document has changed since `old_tree` was
253 | /// created, then you must edit `old_tree` to match the new text using
254 | /// [MarkdownTree::edit].
255 | ///
256 | /// Returns a [MarkdownTree] if parsing succeeded, or `None` if:
257 | /// * The timeout set with [tree_sitter::Parser::set_timeout_micros] expired
258 | /// * The cancellation flag set with [tree_sitter::Parser::set_cancellation_flag] was flipped
259 | pub fn parse_with<T: AsRef<[u8]>, F: FnMut(usize, Point) -> T>(
260 | &mut self,
261 | callback: &mut F,
262 | old_tree: Option<&MarkdownTree>,
263 | ) -> Option<MarkdownTree> {
264 | let MarkdownParser {
265 | parser,
266 | block_language,
267 | inline_language,
268 | } = self;
269 | parser
270 | .set_included_ranges(&[])
271 | .expect("Can not set included ranges to whole document");
272 | parser
273 | .set_language(block_language)
274 | .expect("Could not load block grammar");
275 | let block_tree = parser.parse_with(callback, old_tree.map(|tree| &tree.block_tree))?;
276 | let (mut inline_trees, mut inline_indices) = if let Some(old_tree) = old_tree {
277 | let len = old_tree.inline_trees.len();
278 | (Vec::with_capacity(len), HashMap::with_capacity(len))
279 | } else {
280 | (Vec::new(), HashMap::new())
281 | };
282 | parser
283 | .set_language(inline_language)
284 | .expect("Could not load inline grammar");
285 | let mut tree_cursor = block_tree.walk();
286 |
287 | let mut i = 0;
288 | 'outer: loop {
289 | let node = loop {
290 | let kind = tree_cursor.node().kind();
291 | if kind == "inline" || kind == "pipe_table_cell" || !tree_cursor.goto_first_child()
292 | {
293 | while !tree_cursor.goto_next_sibling() {
294 | if !tree_cursor.goto_parent() {
295 | break 'outer;
296 | }
297 | }
298 | }
299 | let kind = tree_cursor.node().kind();
300 | if kind == "inline" || kind == "pipe_table_cell" {
301 | break tree_cursor.node();
302 | }
303 | };
304 | let mut range = node.range();
305 | let mut ranges = Vec::new();
306 | if tree_cursor.goto_first_child() {
307 | while tree_cursor.goto_next_sibling() {
308 | if !tree_cursor.node().is_named() {
309 | continue;
310 | }
311 | let child_range = tree_cursor.node().range();
312 | ranges.push(Range {
313 | start_byte: range.start_byte,
314 | start_point: range.start_point,
315 | end_byte: child_range.start_byte,
316 | end_point: child_range.start_point,
317 | });
318 | range.start_byte = child_range.end_byte;
319 | range.start_point = child_range.end_point;
320 | }
321 | tree_cursor.goto_parent();
322 | }
323 | ranges.push(range);
324 | parser.set_included_ranges(&ranges).ok()?;
325 | let inline_tree = parser.parse_with(
326 | callback,
327 | old_tree.and_then(|old_tree| old_tree.inline_trees.get(i)),
328 | )?;
329 | inline_trees.push(inline_tree);
330 | inline_indices.insert(node.id(), i);
331 | i += 1;
332 | }
333 | drop(tree_cursor);
334 | inline_trees.shrink_to_fit();
335 | inline_indices.shrink_to_fit();
336 | Some(MarkdownTree {
337 | block_tree,
338 | inline_trees,
339 | inline_indices,
340 | })
341 | }
342 |
343 | /// Parse a slice of UTF8 text.
344 | ///
345 | /// # Arguments:
346 | /// * `text` The UTF8-encoded text to parse.
347 | /// * `old_tree` A previous syntax tree parsed from the same document.
348 | /// If the text of the document has changed since `old_tree` was
349 | /// created, then you must edit `old_tree` to match the new text using
350 | /// [MarkdownTree::edit].
351 | ///
352 | /// Returns a [MarkdownTree] if parsing succeeded, or `None` if:
353 | /// * The timeout set with [tree_sitter::Parser::set_timeout_micros] expired
354 | /// * The cancellation flag set with [tree_sitter::Parser::set_cancellation_flag] was flipped
355 | pub fn parse(&mut self, text: &[u8], old_tree: Option<&MarkdownTree>) -> Option<MarkdownTree> {
356 | self.parse_with(&mut |byte, _| &text[byte..], old_tree)
357 | }
358 | }
359 |
360 | #[cfg(test)]
361 | mod tests {
362 | use tree_sitter::{InputEdit, Point};
363 |
364 | use super::*;
365 |
366 | #[test]
367 | fn inline_ranges() {
368 | let code = "# title\n\nInline [content].\n";
369 | let mut parser = MarkdownParser::default();
370 | let mut tree = parser.parse(code.as_bytes(), None).unwrap();
371 |
372 | let section = tree.block_tree().root_node().child(0).unwrap();
373 | assert_eq!(section.kind(), "section");
374 | let heading = section.child(0).unwrap();
375 | assert_eq!(heading.kind(), "atx_heading");
376 | let paragraph = section.child(1).unwrap();
377 | assert_eq!(paragraph.kind(), "paragraph");
378 | let inline = paragraph.child(0).unwrap();
379 | assert_eq!(inline.kind(), "inline");
380 | assert_eq!(
381 | tree.inline_tree(&inline)
382 | .unwrap()
383 | .root_node()
384 | .child(0)
385 | .unwrap()
386 | .kind(),
387 | "shortcut_link"
388 | );
389 |
390 | let code = "# Title\n\nInline [content].\n";
391 | tree.edit(&InputEdit {
392 | start_byte: 2,
393 | old_end_byte: 3,
394 | new_end_byte: 3,
395 | start_position: Point { row: 0, column: 2 },
396 | old_end_position: Point { row: 0, column: 3 },
397 | new_end_position: Point { row: 0, column: 3 },
398 | });
399 | let tree = parser.parse(code.as_bytes(), Some(&tree)).unwrap();
400 |
401 | let section = tree.block_tree().root_node().child(0).unwrap();
402 | assert_eq!(section.kind(), "section");
403 | let heading = section.child(0).unwrap();
404 | assert_eq!(heading.kind(), "atx_heading");
405 | let paragraph = section.child(1).unwrap();
406 | assert_eq!(paragraph.kind(), "paragraph");
407 | let inline = paragraph.child(0).unwrap();
408 | assert_eq!(inline.kind(), "inline");
409 | assert_eq!(
410 | tree.inline_tree(&inline)
411 | .unwrap()
412 | .root_node()
413 | .named_child(0)
414 | .unwrap()
415 | .kind(),
416 | "shortcut_link"
417 | );
418 | }
419 |
420 | #[test]
421 | fn markdown_cursor() {
422 | let code = "# title\n\nInline [content].\n";
423 | let mut parser = MarkdownParser::default();
424 | let tree = parser.parse(code.as_bytes(), None).unwrap();
425 | let mut cursor = tree.walk();
426 | assert_eq!(cursor.node().kind(), "document");
427 | assert!(cursor.goto_first_child());
428 | assert_eq!(cursor.node().kind(), "section");
429 | assert!(cursor.goto_first_child());
430 | assert_eq!(cursor.node().kind(), "atx_heading");
431 | assert!(cursor.goto_next_sibling());
432 | assert_eq!(cursor.node().kind(), "paragraph");
433 | assert!(cursor.goto_first_child());
434 | assert_eq!(cursor.node().kind(), "inline");
435 | assert!(cursor.goto_first_child());
436 | assert_eq!(cursor.node().kind(), "shortcut_link");
437 | assert!(cursor.goto_parent());
438 | assert!(cursor.goto_parent());
439 | assert!(cursor.goto_parent());
440 | assert!(cursor.goto_parent());
441 | assert_eq!(cursor.node().kind(), "document");
442 | }
443 |
444 | #[test]
445 | fn table() {
446 | let code = "| foo |\n| --- |\n| *bar*|\n";
447 | let mut parser = MarkdownParser::default();
448 | let tree = parser.parse(code.as_bytes(), None).unwrap();
449 | dbg!(&tree.inline_trees());
450 | let mut cursor = tree.walk();
451 |
452 | assert_eq!(cursor.node().kind(), "document");
453 | assert!(cursor.goto_first_child());
454 | assert_eq!(cursor.node().kind(), "section");
455 | assert!(cursor.goto_first_child());
456 | assert_eq!(cursor.node().kind(), "pipe_table");
457 | assert!(cursor.goto_first_child());
458 | assert!(cursor.goto_next_sibling());
459 | assert!(cursor.goto_next_sibling());
460 | assert_eq!(cursor.node().kind(), "pipe_table_row");
461 | assert!(cursor.goto_first_child());
462 | assert!(cursor.goto_next_sibling());
463 | assert_eq!(cursor.node().kind(), "pipe_table_cell");
464 | assert!(cursor.goto_first_child());
465 | assert_eq!(cursor.node().kind(), "emphasis");
466 | }
467 | }
468 |
```
--------------------------------------------------------------------------------
/deps/tree-sitter-markdown/tree-sitter-markdown-inline/grammar.js:
--------------------------------------------------------------------------------
```javascript
1 | // This grammar only concerns the inline structure according to the CommonMark Spec
2 | // (https://spec.commonmark.org/0.30/#inlines)
3 | // For more information see README.md
4 |
5 | /// <reference types="tree-sitter-cli/dsl" />
6 |
7 | const common = require('../common/common');
8 |
9 | // Levels used for dynmic precedence. Ideally
10 | // n * PRECEDENCE_LEVEL_EMPHASIS > PRECEDENCE_LEVEL_LINK for any n, so maybe the
11 | // maginuted of these values should be increased in the future
12 | const PRECEDENCE_LEVEL_EMPHASIS = 1;
13 | const PRECEDENCE_LEVEL_LINK = 10;
14 | const PRECEDENCE_LEVEL_HTML = 100;
15 |
16 | // Punctuation characters as specified in
17 | // https://github.github.com/gfm/#ascii-punctuation-character
18 | const PUNCTUATION_CHARACTERS_REGEX = '!-/:-@\\[-`\\{-~';
19 |
20 |
21 | // !!!
22 | // Notice the call to `add_inline_rules` which generates some additional rules related to parsing
23 | // inline contents in different contexts.
24 | // !!!
25 | module.exports = grammar(add_inline_rules({
26 | name: 'markdown_inline',
27 |
28 | externals: $ => [
29 | // An `$._error` token is never valid and gets emmited to kill invalid parse branches. Concretely
30 | // this is used to decide wether a newline closes a paragraph and together and it gets emitted
31 | // when trying to parse the `$._trigger_error` token in `$.link_title`.
32 | $._error,
33 | $._trigger_error,
34 |
35 | // Opening and closing delimiters for code spans. These are sequences of one or more backticks.
36 | // An opening token does not mean the text after has to be a code span if there is no closing token
37 | $._code_span_start,
38 | $._code_span_close,
39 |
40 | // Opening and closing delimiters for emphasis.
41 | $._emphasis_open_star,
42 | $._emphasis_open_underscore,
43 | $._emphasis_close_star,
44 | $._emphasis_close_underscore,
45 |
46 | // For emphasis we need to tell the parser if the last character was a whitespace (or the
47 | // beginning of a line) or a punctuation. These tokens never actually get emitted.
48 | $._last_token_whitespace,
49 | $._last_token_punctuation,
50 |
51 | $._strikethrough_open,
52 | $._strikethrough_close,
53 |
54 | // Opening and closing delimiters for latex. These are sequences of one or more dollar signs.
55 | // An opening token does not mean the text after has to be latex if there is no closing token
56 | $._latex_span_start,
57 | $._latex_span_close,
58 |
59 | // Token emmited when encountering opening delimiters for a leaf span
60 | // e.g. a code span, that does not have a matching closing span
61 | $._unclosed_span
62 | ],
63 | precedences: $ => [
64 | // [$._strong_emphasis_star, $._inline_element_no_star],
65 | [$._strong_emphasis_star_no_link, $._inline_element_no_star_no_link],
66 | // [$._strong_emphasis_underscore, $._inline_element_no_underscore],
67 | [$._strong_emphasis_underscore_no_link, $._inline_element_no_underscore_no_link],
68 | [$.hard_line_break, $._whitespace],
69 | [$.hard_line_break, $._text_base],
70 | ],
71 | // More conflicts are defined in `add_inline_rules`
72 | conflicts: $ => [
73 |
74 | [$._closing_tag, $._text_base],
75 | [$._open_tag, $._text_base],
76 | [$._html_comment, $._text_base],
77 | [$._processing_instruction, $._text_base],
78 | [$._declaration, $._text_base],
79 | [$._cdata_section, $._text_base],
80 |
81 | [$._link_text_non_empty, $._inline_element],
82 | [$._link_text_non_empty, $._inline_element_no_star],
83 | [$._link_text_non_empty, $._inline_element_no_underscore],
84 | [$._link_text_non_empty, $._inline_element_no_tilde],
85 | [$._link_text, $._inline_element],
86 | [$._link_text, $._inline_element_no_star],
87 | [$._link_text, $._inline_element_no_underscore],
88 | [$._link_text, $._inline_element_no_tilde],
89 |
90 | [$._image_description, $._image_description_non_empty, $._text_base],
91 | // [$._image_description, $._image_description_non_empty, $._text_inline],
92 | // [$._image_description, $._image_description_non_empty, $._text_inline_no_star],
93 | // [$._image_description, $._image_description_non_empty, $._text_inline_no_underscore],
94 |
95 | [$._image_shortcut_link, $._image_description],
96 | [$.shortcut_link, $._link_text],
97 | [$.link_destination, $.link_title],
98 | [$._link_destination_parenthesis, $.link_title],
99 |
100 | [$.wiki_link, $._inline_element],
101 | [$.wiki_link, $._inline_element_no_star],
102 | [$.wiki_link, $._inline_element_no_underscore],
103 | [$.wiki_link, $._inline_element_no_tilde],
104 | ],
105 | extras: $ => [],
106 |
107 | rules: {
108 | inline: $ => seq(optional($._last_token_whitespace), $._inline),
109 |
110 | ...common.rules,
111 |
112 |
113 | // A lot of inlines are defined in `add_inline_rules`, including:
114 | //
115 | // * collections of inlines
116 | // * emphasis
117 | // * textual content
118 | //
119 | // This is done to reduce code duplication, as some inlines need to be parsed differently
120 | // depending on the context. For example inlines in ATX headings may not contain newlines.
121 |
122 | code_span: $ => seq(
123 | alias($._code_span_start, $.code_span_delimiter),
124 | repeat(choice($._text_base, '[', ']', $._soft_line_break, $._html_tag)),
125 | alias($._code_span_close, $.code_span_delimiter)
126 | ),
127 |
128 | latex_block: $ => seq(
129 | alias($._latex_span_start, $.latex_span_delimiter),
130 | repeat(choice($._text_base, '[', ']', $._soft_line_break, $._html_tag, $.backslash_escape)),
131 | alias($._latex_span_close, $.latex_span_delimiter),
132 | ),
133 |
134 | // Different kinds of links:
135 | // * inline links (https://github.github.com/gfm/#inline-link)
136 | // * full reference links (https://github.github.com/gfm/#full-reference-link)
137 | // * collapsed reference links (https://github.github.com/gfm/#collapsed-reference-link)
138 | // * shortcut links (https://github.github.com/gfm/#shortcut-reference-link)
139 | //
140 | // Dynamic precedence is distributed as granular as possible to help the parser decide
141 | // while parsing which branch is the most important.
142 | //
143 | // https://github.github.com/gfm/#links
144 | _link_text: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, choice(
145 | $._link_text_non_empty,
146 | seq('[', ']')
147 | )),
148 | _link_text_non_empty: $ => seq('[', alias($._inline_no_link, $.link_text), ']'),
149 | shortcut_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, $._link_text_non_empty),
150 | full_reference_link: $ => prec.dynamic(2 * PRECEDENCE_LEVEL_LINK, seq(
151 | $._link_text,
152 | $.link_label
153 | )),
154 | collapsed_reference_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq(
155 | $._link_text,
156 | '[',
157 | ']'
158 | )),
159 | inline_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq(
160 | $._link_text,
161 | '(',
162 | repeat(choice($._whitespace, $._soft_line_break)),
163 | optional(seq(
164 | choice(
165 | seq(
166 | $.link_destination,
167 | optional(seq(
168 | repeat1(choice($._whitespace, $._soft_line_break)),
169 | $.link_title
170 | ))
171 | ),
172 | $.link_title,
173 | ),
174 | repeat(choice($._whitespace, $._soft_line_break)),
175 | )),
176 | ')'
177 | )),
178 |
179 | wiki_link: $ => prec.dynamic(2 * PRECEDENCE_LEVEL_LINK, seq(
180 | '[', '[',
181 | alias($._wiki_link_destination, $.link_destination),
182 | optional(seq(
183 | '|',
184 | alias($._wiki_link_text, $.link_text)
185 | )),
186 | ']', ']'
187 | )
188 | ),
189 |
190 | _wiki_link_destination: $ => repeat1(choice(
191 | $._word,
192 | common.punctuation_without($, ['[',']', '|']),
193 | $._whitespace,
194 | )),
195 |
196 | _wiki_link_text: $ => repeat1(choice(
197 | $._word,
198 | common.punctuation_without($, ['[',']']),
199 | $._whitespace,
200 | )),
201 |
202 | // Images work exactly like links with a '!' added in front.
203 | //
204 | // https://github.github.com/gfm/#images
205 | image: $ => choice(
206 | $._image_inline_link,
207 | $._image_shortcut_link,
208 | $._image_full_reference_link,
209 | $._image_collapsed_reference_link
210 | ),
211 | _image_inline_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq(
212 | $._image_description,
213 | '(',
214 | repeat(choice($._whitespace, $._soft_line_break)),
215 | optional(seq(
216 | choice(
217 | seq(
218 | $.link_destination,
219 | optional(seq(
220 | repeat1(choice($._whitespace, $._soft_line_break)),
221 | $.link_title
222 | ))
223 | ),
224 | $.link_title,
225 | ),
226 | repeat(choice($._whitespace, $._soft_line_break)),
227 | )),
228 | ')'
229 | )),
230 | _image_shortcut_link: $ => prec.dynamic(3 * PRECEDENCE_LEVEL_LINK, $._image_description_non_empty),
231 | _image_full_reference_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq($._image_description, $.link_label)),
232 | _image_collapsed_reference_link: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq($._image_description, '[', ']')),
233 | _image_description: $ => prec.dynamic(3 * PRECEDENCE_LEVEL_LINK, choice($._image_description_non_empty, seq('!', '[', prec(1, ']')))),
234 | _image_description_non_empty: $ => seq('!', '[', alias($._inline, $.image_description), prec(1, ']')),
235 |
236 | // Autolinks. Uri autolinks actually accept protocolls of arbitrary length which does not
237 | // align with the spec. This is because the binary for the grammar gets to large if done
238 | // otherwise as tree-sitters code generation is not very concise for this type of regex.
239 | //
240 | // Email autolinks do not match every valid email (emails normally should not be parsed
241 | // using regexes), but this is how they are defined in the spec.
242 | //
243 | // https://github.github.com/gfm/#autolinks
244 | uri_autolink: $ => /<[a-zA-Z][a-zA-Z0-9+\.\-][a-zA-Z0-9+\.\-]*:[^ \t\r\n<>]*>/,
245 | email_autolink: $ =>
246 | /<[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/,
247 |
248 | // Raw html. As with html blocks we do not emit additional information as this is best done
249 | // by a proper html tree-sitter grammar.
250 | //
251 | // https://github.github.com/gfm/#raw-html
252 | _html_tag: $ => choice($._open_tag, $._closing_tag, $._html_comment, $._processing_instruction, $._declaration, $._cdata_section),
253 | _open_tag: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq('<', $._tag_name, repeat($._attribute), repeat(choice($._whitespace, $._soft_line_break)), optional('/'), '>')),
254 | _closing_tag: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq('<', '/', $._tag_name, repeat(choice($._whitespace, $._soft_line_break)), '>')),
255 | _tag_name: $ => seq($._word_no_digit, repeat(choice($._word_no_digit, $._digits, '-'))),
256 | _attribute: $ => seq(repeat1(choice($._whitespace, $._soft_line_break)), $._attribute_name, repeat(choice($._whitespace, $._soft_line_break)), '=', repeat(choice($._whitespace, $._soft_line_break)), $._attribute_value),
257 | _attribute_name: $ => /[a-zA-Z_:][a-zA-Z0-9_\.:\-]*/,
258 | _attribute_value: $ => choice(
259 | /[^ \t\r\n"'=<>`]+/,
260 | seq("'", repeat(choice($._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ["'"]))), "'"),
261 | seq('"', repeat(choice($._word, $._whitespace, $._soft_line_break, common.punctuation_without($, ['"']))), '"'),
262 | ),
263 | _html_comment: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq(
264 | '<!--',
265 | optional(seq(
266 | choice(
267 | $._word,
268 | $._whitespace,
269 | $._soft_line_break,
270 | common.punctuation_without($, ['-', '>']),
271 | seq(
272 | '-',
273 | common.punctuation_without($, ['>']),
274 | )
275 | ),
276 | repeat(prec.right(choice(
277 | $._word,
278 | $._whitespace,
279 | $._soft_line_break,
280 | common.punctuation_without($, ['-']),
281 | seq(
282 | '-',
283 | choice(
284 | $._word,
285 | $._whitespace,
286 | $._soft_line_break,
287 | common.punctuation_without($, ['-']),
288 | )
289 | )
290 | ))),
291 | )),
292 | '-->'
293 | )),
294 | _processing_instruction: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq(
295 | '<?',
296 | repeat(prec.right(choice(
297 | $._word,
298 | $._whitespace,
299 | $._soft_line_break,
300 | common.punctuation_without($, []),
301 | ))),
302 | '?>'
303 | )),
304 | _declaration: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq(
305 | /<![A-Z]+/,
306 | choice(
307 | $._whitespace,
308 | $._soft_line_break,
309 | ),
310 | repeat(prec.right(choice(
311 | $._word,
312 | $._whitespace,
313 | $._soft_line_break,
314 | common.punctuation_without($, ['>']),
315 | ))),
316 | '>'
317 | )),
318 | _cdata_section: $ => prec.dynamic(PRECEDENCE_LEVEL_HTML, seq(
319 | '<![CDATA[',
320 | repeat(prec.right(choice(
321 | $._word,
322 | $._whitespace,
323 | $._soft_line_break,
324 | common.punctuation_without($, []),
325 | ))),
326 | ']]>'
327 | )),
328 |
329 | // A hard line break.
330 | //
331 | // https://github.github.com/gfm/#hard-line-breaks
332 | hard_line_break: $ => seq(choice('\\', $._whitespace_ge_2), $._soft_line_break),
333 | _text: $ => choice($._word, common.punctuation_without($, []), $._whitespace),
334 |
335 | // Whitespace is divided into single whitespaces and multiple whitespaces as wee need this
336 | // information for hard line breaks.
337 | _whitespace_ge_2: $ => /\t| [ \t]+/,
338 | _whitespace: $ => seq(choice($._whitespace_ge_2, / /), optional($._last_token_whitespace)),
339 |
340 | // Other than whitespace we tokenize into strings of digits, punctuation characters
341 | // (handled by `common.punctuation_without`) and strings of any other characters. This way the
342 | // lexer does not have to many different states, which makes it a lot easier to make
343 | // conflicts work.
344 | _word: $ => choice($._word_no_digit, $._digits),
345 | _word_no_digit: $ => new RegExp('[^' + PUNCTUATION_CHARACTERS_REGEX + ' \\t\\n\\r0-9]+(_+[^' + PUNCTUATION_CHARACTERS_REGEX + ' \\t\\n\\r0-9]+)*'),
346 | _digits: $ => /[0-9][0-9_]*/,
347 | _soft_line_break: $ => seq($._newline_token, optional($._last_token_whitespace)),
348 |
349 | _inline_base: $ => prec.right(repeat1(choice(
350 | $.image,
351 | $._soft_line_break,
352 | $.backslash_escape,
353 | $.hard_line_break,
354 | $.uri_autolink,
355 | $.email_autolink,
356 | $.entity_reference,
357 | $.numeric_character_reference,
358 | (common.EXTENSION_LATEX ? $.latex_block : choice()),
359 | $.code_span,
360 | alias($._html_tag, $.html_tag),
361 | $._text_base,
362 | common.EXTENSION_TAGS ? $.tag : choice(),
363 | $._unclosed_span,
364 | ))),
365 | _text_base: $ => choice(
366 | $._word,
367 | common.punctuation_without($, ['[', ']']),
368 | $._whitespace,
369 | '<!--',
370 | /<![A-Z]+/,
371 | '<?',
372 | '<![CDATA[',
373 | ),
374 | _text_inline_no_link: $ => choice(
375 | $._text_base,
376 | $._emphasis_open_star,
377 | $._emphasis_open_underscore,
378 | $._unclosed_span,
379 | ),
380 |
381 | ...(common.EXTENSION_TAGS ? {
382 | tag: $ => /#[0-9]*[a-zA-Z_\-\/][a-zA-Z_\-\/0-9]*/,
383 | } : {}),
384 |
385 | },
386 | }));
387 |
388 | // This function adds some extra inline rules. This is done to reduce code duplication, as some
389 | // rules may not contain newlines, characters like '*' and '_', ... depending on the context.
390 | //
391 | // This is by far the most ugly part of this code and should be cleaned up.
392 | function add_inline_rules(grammar) {
393 | let conflicts = [];
394 | for (let link of [true, false]) {
395 | let suffix_link = link ? "" : "_no_link";
396 | for (let delimiter of [false, "star", "underscore", "tilde"]) {
397 | let suffix_delimiter = delimiter ? "_no_" + delimiter : "";
398 | let suffix = suffix_delimiter + suffix_link;
399 | grammar.rules["_inline_element" + suffix] = $ => {
400 | let elements = [
401 | $._inline_base,
402 | alias($['_emphasis_star' + suffix_link], $.emphasis),
403 | alias($['_strong_emphasis_star' + suffix_link], $.strong_emphasis),
404 | alias($['_emphasis_underscore' + suffix_link], $.emphasis),
405 | alias($['_strong_emphasis_underscore' + suffix_link], $.strong_emphasis),
406 | ];
407 | if (common.EXTENSION_STRIKETHROUGH) {
408 | elements.push(alias($['_strikethrough' + suffix_link], $.strikethrough));
409 | }
410 | if (delimiter !== "star") {
411 | elements.push($._emphasis_open_star);
412 | }
413 | if (delimiter !== "underscore") {
414 | elements.push($._emphasis_open_underscore);
415 | }
416 | if (delimiter !== "tilde") {
417 | elements.push($._strikethrough_open);
418 | }
419 | if (link) {
420 | elements = elements.concat([
421 | $.shortcut_link,
422 | $.full_reference_link,
423 | $.collapsed_reference_link,
424 | $.inline_link,
425 | // (common.EXTENSION_WIKI_LINK && $.wiki_link),
426 | seq(choice('[', ']'), optional($._last_token_punctuation)),
427 | ]);
428 | if (common.EXTENSION_WIKI_LINK) {
429 | elements.push($.wiki_link);
430 | }
431 | }
432 | return choice(...elements);
433 | };
434 | grammar.rules["_inline" + suffix] = $ => repeat1($["_inline_element" + suffix]);
435 | if (delimiter !== "star") {
436 | conflicts.push(['_emphasis_star' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]);
437 | conflicts.push(['_emphasis_star' + suffix_link, '_strong_emphasis_star' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]);
438 | }
439 | if (delimiter == 'star' || delimiter == 'underscore') {
440 | conflicts.push(['_strong_emphasis_' + delimiter + suffix_link, '_inline_element_no_' + delimiter]);
441 | }
442 | if (delimiter !== "underscore") {
443 | conflicts.push(['_emphasis_underscore' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]);
444 | conflicts.push(['_emphasis_underscore' + suffix_link, '_strong_emphasis_underscore' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]);
445 | }
446 | if (delimiter !== "tilde") {
447 | conflicts.push(['_strikethrough' + suffix_link, '_inline_element' + suffix_delimiter + suffix_link]);
448 | }
449 | }
450 |
451 | if (common.EXTENSION_STRIKETHROUGH) {
452 | grammar.rules['_strikethrough' + suffix_link] = $ => prec.dynamic(PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._strikethrough_open, $.emphasis_delimiter), optional($._last_token_punctuation), $['_inline' + '_no_tilde' + suffix_link], alias($._strikethrough_close, $.emphasis_delimiter)));
453 | }
454 | grammar.rules['_emphasis_star' + suffix_link] = $ => prec.dynamic(PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_star, $.emphasis_delimiter), optional($._last_token_punctuation), $['_inline' + '_no_star' + suffix_link], alias($._emphasis_close_star, $.emphasis_delimiter)));
455 | grammar.rules['_strong_emphasis_star' + suffix_link] = $ => prec.dynamic(2 * PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_star, $.emphasis_delimiter), $['_emphasis_star' + suffix_link], alias($._emphasis_close_star, $.emphasis_delimiter)));
456 | grammar.rules['_emphasis_underscore' + suffix_link] = $ => prec.dynamic(PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_underscore, $.emphasis_delimiter), optional($._last_token_punctuation), $['_inline' + '_no_underscore' + suffix_link], alias($._emphasis_close_underscore, $.emphasis_delimiter)));
457 | grammar.rules['_strong_emphasis_underscore' + suffix_link] = $ => prec.dynamic(2 * PRECEDENCE_LEVEL_EMPHASIS, seq(alias($._emphasis_open_underscore, $.emphasis_delimiter), $['_emphasis_underscore' + suffix_link], alias($._emphasis_close_underscore, $.emphasis_delimiter)));
458 | }
459 |
460 | let old = grammar.conflicts
461 | grammar.conflicts = $ => {
462 | let cs = old($);
463 | for (let conflict of conflicts) {
464 | let c = [];
465 | for (let rule of conflict) {
466 | c.push($[rule]);
467 | }
468 | cs.push(c);
469 | }
470 | return cs;
471 | }
472 |
473 | return grammar;
474 | }
475 |
```
--------------------------------------------------------------------------------
/src/ui/style_manager.cpp:
--------------------------------------------------------------------------------
```cpp
1 | #include "style_manager.h"
2 | #include <algorithm>
3 | #include <cctype>
4 | #include <cstring>
5 | #include <fstream>
6 | #include <iostream>
7 | #ifdef _WIN32
8 | #include <curses.h>
9 | inline int setenv(const char *name, const char *value, int overwrite)
10 | {
11 | if (!overwrite)
12 | {
13 | size_t envsize = 0;
14 | getenv_s(&envsize, nullptr, 0, name);
15 | if (envsize != 0)
16 | return 0; // Variable exists, don't overwrite
17 | }
18 | return _putenv_s(name, value);
19 | }
20 | #else
21 | #include <ncurses.h>
22 | #endif
23 | #include <sstream>
24 |
25 | StyleManager::StyleManager()
26 | : initialized(false), supports_256_colors_cache(false),
27 | next_custom_color_id(16)
28 | {
29 | }
30 |
31 | void StyleManager::initialize()
32 | {
33 | if (initialized)
34 | {
35 | std::cerr << "StyleManager already initialized" << std::endl;
36 | return;
37 | }
38 |
39 | // Critical: Check if ncurses has been initialized first
40 | if (!stdscr)
41 | {
42 | std::cerr << "ERROR: ncurses not initialized. Call initscr() first!"
43 | << std::endl;
44 | return;
45 | }
46 |
47 | if (!has_colors())
48 | {
49 | std::cerr << "Terminal does not support colors" << std::endl;
50 | return;
51 | }
52 |
53 | // Initialize color support
54 | if (start_color() == ERR)
55 | {
56 | std::cerr << "Failed to start color support" << std::endl;
57 | return;
58 | }
59 |
60 | // Use default terminal colors - CRITICAL for transparent support
61 | if (use_default_colors() == ERR)
62 | {
63 | std::cerr << "Warning: use_default_colors() failed, using fallback"
64 | << std::endl;
65 | // Fallback: assume black background, white foreground
66 | assume_default_colors(COLOR_WHITE, COLOR_BLACK);
67 | }
68 |
69 | // Initialize color capability cache and custom color tracking
70 | supports_256_colors_cache = supports_256_colors();
71 | next_custom_color_id = 16; // Start custom colors at ID 16
72 | color_cache.clear();
73 |
74 | // std::cerr << "=== UNIFIED THEME SYSTEM WITH HEX SUPPORT ===" << std::endl;
75 | // std::cerr << "COLORS: " << COLORS << std::endl;
76 | // std::cerr << "COLOR_PAIRS: " << COLOR_PAIRS << std::endl;
77 | // std::cerr << "256-color support: "
78 | // << (supports_256_colors_cache ? "YES" : "NO") << std::endl;
79 | // std::cerr << "TERM: " << (getenv("TERM") ? getenv("TERM") : "not set")
80 | // << std::endl;
81 |
82 | // Check for WSL-specific issues
83 | const char *wsl_distro = getenv("WSL_DISTRO_NAME");
84 | if (wsl_distro)
85 | {
86 | // std::cerr << "WSL detected: " << wsl_distro << std::endl;
87 | // WSL sometimes has color issues, force TERM if needed
88 | if (!getenv("TERM") || strcmp(getenv("TERM"), "dumb") == 0)
89 | {
90 | // std::cerr << "Setting TERM=xterm-256color for WSL" << std::endl;
91 | setenv("TERM", "xterm-256color", 1);
92 | }
93 | }
94 |
95 | load_default_theme();
96 | apply_theme();
97 |
98 | initialized = true;
99 | // std::cerr << "Unified theme system initialized successfully" << std::endl;
100 | }
101 |
102 | short StyleManager::resolve_theme_color(const std::string &config_value)
103 | {
104 | // Handle transparent/default colors
105 | if (config_value.empty() || config_value == "transparent" ||
106 | config_value == "default")
107 | {
108 | return -1; // Use terminal default
109 | }
110 |
111 | // Check if it's a hex color
112 | if (config_value.length() == 7 && config_value[0] == '#')
113 | {
114 | // Check cache first
115 | auto cache_it = color_cache.find(config_value);
116 | if (cache_it != color_cache.end())
117 | {
118 | return cache_it->second;
119 | }
120 |
121 | // Parse the hex color
122 | RGB rgb = parse_hex_color(config_value);
123 |
124 | if (supports_256_colors_cache && next_custom_color_id < COLORS)
125 | {
126 | // 256-color mode: use init_color for true color mapping
127 | // Scale R, G, B from 0-255 to ncurses' 0-1000 range
128 | short r_1000 = (rgb.r * 1000) / 255;
129 | short g_1000 = (rgb.g * 1000) / 255;
130 | short b_1000 = (rgb.b * 1000) / 255;
131 |
132 | short color_id = next_custom_color_id;
133 | if (init_color(color_id, r_1000, g_1000, b_1000) == OK)
134 | {
135 | // Cache the mapping and increment for next time
136 | auto cache_it = color_cache.find(config_value);
137 | if (cache_it != color_cache.end())
138 | {
139 | return cache_it->second; // OK: using iterator
140 | }
141 |
142 | color_cache[config_value] = color_id;
143 | const_cast<StyleManager *>(this)->next_custom_color_id++;
144 |
145 | // std::cerr << "Created custom color " << color_id << " for "
146 | // << config_value << " (RGB: " << rgb.r << "," << rgb.g <<
147 | // ","
148 | // << rgb.b << ")" << std::endl;
149 | return color_id;
150 | }
151 | else
152 | {
153 | std::cerr << "Failed to create custom color for " << config_value
154 | << ", falling back to closest 8-color" << std::endl;
155 | }
156 | }
157 |
158 | // Fallback to legacy 8-color mode
159 | return find_closest_8color(rgb);
160 | }
161 |
162 | // Legacy named color support (for backward compatibility)
163 | // This handles old theme files that might still use color names
164 | ThemeColor legacy_color = string_to_theme_color(config_value);
165 | return theme_color_to_ncurses_color(legacy_color);
166 | }
167 |
168 | // NEW: Hex color parsing utility
169 | RGB StyleManager::parse_hex_color(const std::string &hex_str) const
170 | {
171 | RGB rgb;
172 |
173 | if (hex_str.length() != 7 || hex_str[0] != '#')
174 | {
175 | std::cerr << "Invalid hex color format: " << hex_str << ", using black"
176 | << std::endl;
177 | return RGB(0, 0, 0);
178 | }
179 |
180 | try
181 | {
182 | // Parse each component (skip the '#')
183 | std::string r_str = hex_str.substr(1, 2);
184 | std::string g_str = hex_str.substr(3, 2);
185 | std::string b_str = hex_str.substr(5, 2);
186 |
187 | rgb.r = std::stoi(r_str, nullptr, 16);
188 | rgb.g = std::stoi(g_str, nullptr, 16);
189 | rgb.b = std::stoi(b_str, nullptr, 16);
190 |
191 | // Clamp to valid range
192 | rgb.r = std::max(0, std::min(255, rgb.r));
193 | rgb.g = std::max(0, std::min(255, rgb.g));
194 | rgb.b = std::max(0, std::min(255, rgb.b));
195 | }
196 | catch (const std::exception &e)
197 | {
198 | std::cerr << "Error parsing hex color " << hex_str << ": " << e.what()
199 | << std::endl;
200 | return RGB(0, 0, 0);
201 | }
202 |
203 | return rgb;
204 | }
205 |
206 | // NEW: Find closest 8-color match for fallback
207 | short StyleManager::find_closest_8color(const RGB &rgb) const
208 | {
209 | // Define the standard 8 colors in RGB
210 | struct ColorMapping
211 | {
212 | short ncurses_color;
213 | RGB rgb;
214 | const char *name;
215 | };
216 |
217 | static const ColorMapping basic_colors[] = {
218 | {COLOR_BLACK, RGB(0, 0, 0), "black"},
219 | {COLOR_RED, RGB(128, 0, 0), "red"},
220 | {COLOR_GREEN, RGB(0, 128, 0), "green"},
221 | {COLOR_YELLOW, RGB(128, 128, 0), "yellow"},
222 | {COLOR_BLUE, RGB(0, 0, 128), "blue"},
223 | {COLOR_MAGENTA, RGB(128, 0, 128), "magenta"},
224 | {COLOR_CYAN, RGB(0, 128, 128), "cyan"},
225 | {COLOR_WHITE, RGB(192, 192, 192), "white"}};
226 |
227 | // Find the closest color using simple Euclidean distance
228 | double min_distance = 1000000;
229 | short closest_color = COLOR_WHITE;
230 |
231 | for (const auto &color : basic_colors)
232 | {
233 | double dr = rgb.r - color.rgb.r;
234 | double dg = rgb.g - color.rgb.g;
235 | double db = rgb.b - color.rgb.b;
236 | double distance = dr * dr + dg * dg + db * db;
237 |
238 | if (distance < min_distance)
239 | {
240 | min_distance = distance;
241 | closest_color = color.ncurses_color;
242 | }
243 | }
244 |
245 | return closest_color;
246 | }
247 |
248 | // Legacy function - kept only for load_default_theme compatibility
249 | ThemeColor
250 | StyleManager::string_to_theme_color(const std::string &color_name) const
251 | {
252 | std::string lower_name = color_name;
253 | std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(),
254 | ::tolower);
255 |
256 | if (lower_name == "black")
257 | return ThemeColor::BLACK;
258 | if (lower_name == "dark_gray" || lower_name == "dark_grey")
259 | return ThemeColor::DARK_GRAY;
260 | if (lower_name == "gray" || lower_name == "grey")
261 | return ThemeColor::GRAY;
262 | if (lower_name == "light_gray" || lower_name == "light_grey")
263 | return ThemeColor::LIGHT_GRAY;
264 | if (lower_name == "white")
265 | return ThemeColor::WHITE;
266 | if (lower_name == "red")
267 | return ThemeColor::RED;
268 | if (lower_name == "green")
269 | return ThemeColor::GREEN;
270 | if (lower_name == "blue")
271 | return ThemeColor::BLUE;
272 | if (lower_name == "yellow")
273 | return ThemeColor::YELLOW;
274 | if (lower_name == "magenta")
275 | return ThemeColor::MAGENTA;
276 | if (lower_name == "cyan")
277 | return ThemeColor::CYAN;
278 | if (lower_name == "bright_red")
279 | return ThemeColor::BRIGHT_RED;
280 | if (lower_name == "bright_green")
281 | return ThemeColor::BRIGHT_GREEN;
282 | if (lower_name == "bright_blue")
283 | return ThemeColor::BRIGHT_BLUE;
284 | if (lower_name == "bright_yellow")
285 | return ThemeColor::BRIGHT_YELLOW;
286 | if (lower_name == "bright_magenta")
287 | return ThemeColor::BRIGHT_MAGENTA;
288 | if (lower_name == "bright_cyan")
289 | return ThemeColor::BRIGHT_CYAN;
290 |
291 | std::cerr << "Unknown color name: " << color_name << ", using white"
292 | << std::endl;
293 | return ThemeColor::WHITE;
294 | }
295 |
296 | // Legacy function - kept for backward compatibility
297 | int StyleManager::theme_color_to_ncurses_color(ThemeColor color) const
298 | {
299 | switch (color)
300 | {
301 | case ThemeColor::BLACK:
302 | return COLOR_BLACK;
303 | case ThemeColor::DARK_GRAY:
304 | return COLOR_BLACK; // Will use A_BOLD attribute for brighter black
305 | case ThemeColor::GRAY:
306 | return COLOR_WHITE; // Will use A_DIM attribute for dimmed white
307 | case ThemeColor::LIGHT_GRAY:
308 | return COLOR_WHITE; // Will use A_DIM + A_BOLD for medium brightness
309 | case ThemeColor::WHITE:
310 | return COLOR_WHITE;
311 | case ThemeColor::RED:
312 | return COLOR_RED;
313 | case ThemeColor::GREEN:
314 | return COLOR_GREEN;
315 | case ThemeColor::BLUE:
316 | return COLOR_BLUE;
317 | case ThemeColor::YELLOW:
318 | return COLOR_YELLOW;
319 | case ThemeColor::MAGENTA:
320 | return COLOR_MAGENTA;
321 | case ThemeColor::CYAN:
322 | return COLOR_CYAN;
323 | case ThemeColor::BRIGHT_RED:
324 | return COLOR_RED; // Will use A_BOLD attribute
325 | case ThemeColor::BRIGHT_GREEN:
326 | return COLOR_GREEN; // Will use A_BOLD attribute
327 | case ThemeColor::BRIGHT_BLUE:
328 | return COLOR_BLUE; // Will use A_BOLD attribute
329 | case ThemeColor::BRIGHT_YELLOW:
330 | return COLOR_YELLOW; // Will use A_BOLD attribute
331 | case ThemeColor::BRIGHT_MAGENTA:
332 | return COLOR_MAGENTA; // Will use A_BOLD attribute
333 | case ThemeColor::BRIGHT_CYAN:
334 | return COLOR_CYAN; // Will use A_BOLD attribute
335 | case ThemeColor::TERMINAL:
336 | return COLOR_RED;
337 | default:
338 | return COLOR_WHITE;
339 | }
340 | }
341 |
342 | // Legacy function - simplified since 256-color provides enough fidelity
343 | int StyleManager::theme_color_to_ncurses_attr(ThemeColor color) const
344 | {
345 | // With 256-color support, we can rely more on actual colors than attributes
346 | // Keep only essential attributes
347 | switch (color)
348 | {
349 | case ThemeColor::BRIGHT_RED:
350 | case ThemeColor::BRIGHT_GREEN:
351 | case ThemeColor::BRIGHT_BLUE:
352 | case ThemeColor::BRIGHT_YELLOW:
353 | case ThemeColor::BRIGHT_MAGENTA:
354 | case ThemeColor::BRIGHT_CYAN:
355 | return A_BOLD; // Keep bold for legacy compatibility
356 | default:
357 | return A_NORMAL;
358 | }
359 | }
360 |
361 | bool StyleManager::is_light_theme() const
362 | {
363 | // Check if background is a light hex color or legacy light theme
364 | if (current_theme.background[0] == '#')
365 | {
366 | RGB bg_rgb = parse_hex_color(current_theme.background);
367 | // Consider it light if the average RGB value is > 128
368 | return (bg_rgb.r + bg_rgb.g + bg_rgb.b) / 3 > 128;
369 | }
370 |
371 | // Legacy check for named colors
372 | ThemeColor legacy_bg = string_to_theme_color(current_theme.background);
373 | return (legacy_bg == ThemeColor::WHITE ||
374 | legacy_bg == ThemeColor::LIGHT_GRAY);
375 | }
376 |
377 | void StyleManager::load_default_theme()
378 | {
379 | current_theme = {
380 | "Default Dark (Hex)",
381 | "#000000", // background
382 | "#FFFFFF", // foreground
383 | "#FFFFFF", // cursor
384 | "#0000FF", // selection
385 | "#333333", // line_highlight
386 | "#FFFF00", // line_numbers
387 | "#FFFF99", // line_numbers_active
388 | "#000080", // status_bar_bg
389 | "#FFFFFF", // status_bar_fg
390 | "#00FFFF", // status_bar_active
391 |
392 | // Semantic categories
393 | "#569CD6", // keyword
394 | "#CE9178", // string_literal
395 | "#B5CEA8", // number
396 | "#6A9955", // comment
397 | "#DCDCAA", // function_name
398 | "#9CDCFE", // variable
399 | "#4EC9B0", // type
400 | "#D4D4D4", // operator
401 | "#D4D4D4", // punctuation
402 | "#4FC1FF", // constant
403 | "#4EC9B0", // namespace
404 | "#9CDCFE", // property
405 | "#DCDCAA", // decorator
406 | "#C586C0", // macro
407 | "#569CD6", // label
408 |
409 | // Markup
410 | "#569CD6", // markup_heading
411 | "#D4D4D4", // markup_bold
412 | "#CE9178", // markup_italic
413 | "#CE9178", // markup_code
414 | "#CE9178", // markup_code_block
415 | "#3794FF", // markup_link
416 | "#3794FF", // markup_url
417 | "#6A9955", // markup_list
418 | "#6A9955", // markup_blockquote
419 | "#FF6B6B", // markup_strikethrough
420 | "#6A9955" // markup_quote
421 | };
422 | }
423 |
424 | void StyleManager::apply_theme()
425 | {
426 | if (!initialized)
427 | {
428 | // std::cerr << "StyleManager not initialized, cannot apply theme"
429 | // << std::endl;
430 | return;
431 | }
432 |
433 | // std::cerr << "Applying theme: " << current_theme.name << std::endl;
434 |
435 | short terminal_bg = resolve_theme_color(current_theme.background);
436 | short terminal_fg = resolve_theme_color(current_theme.foreground);
437 |
438 | init_pair(0, terminal_fg, terminal_bg);
439 | const int BACKGROUND_PAIR_ID = 100;
440 | init_pair(BACKGROUND_PAIR_ID, terminal_fg, terminal_bg);
441 |
442 | auto init_pair_enhanced = [&](int pair_id, const std::string &fg_color_str,
443 | const std::string &bg_color_str) -> bool
444 | {
445 | if (pair_id >= COLOR_PAIRS)
446 | return false;
447 | short fg = resolve_theme_color(fg_color_str);
448 | short bg = resolve_theme_color(bg_color_str);
449 | int result = init_pair(pair_id, fg, bg);
450 | return (result == OK);
451 | };
452 |
453 | // UI Elements
454 | init_pair_enhanced(2, current_theme.line_numbers, current_theme.background);
455 | init_pair_enhanced(3, current_theme.line_numbers_active,
456 | current_theme.background);
457 | init_pair_enhanced(4, "#808080", current_theme.background);
458 |
459 | // Status bar
460 | init_pair_enhanced(5, current_theme.status_bar_fg,
461 | current_theme.status_bar_bg);
462 | init_pair_enhanced(6, current_theme.status_bar_fg,
463 | current_theme.status_bar_bg);
464 | init_pair_enhanced(7, current_theme.status_bar_active,
465 | current_theme.status_bar_bg);
466 | init_pair_enhanced(8, "#00FFFF", current_theme.status_bar_bg);
467 | init_pair_enhanced(9, "#FFFF00", current_theme.status_bar_bg);
468 | init_pair_enhanced(10, "#00FF00", current_theme.status_bar_bg);
469 | init_pair_enhanced(11, "#FF00FF", current_theme.status_bar_bg);
470 | init_pair_enhanced(12, "#808080", current_theme.status_bar_bg);
471 |
472 | // Selection and cursor
473 | init_pair_enhanced(13, current_theme.cursor, current_theme.background);
474 | init_pair_enhanced(14, current_theme.foreground, current_theme.selection);
475 | init_pair_enhanced(15, current_theme.foreground,
476 | current_theme.line_highlight);
477 |
478 | // Semantic categories (20-39)
479 | init_pair_enhanced(20, current_theme.keyword, current_theme.background);
480 | init_pair_enhanced(21, current_theme.string_literal,
481 | current_theme.background);
482 | init_pair_enhanced(22, current_theme.number, current_theme.background);
483 | init_pair_enhanced(23, current_theme.comment, current_theme.background);
484 | init_pair_enhanced(24, current_theme.function_name, current_theme.background);
485 | init_pair_enhanced(25, current_theme.variable, current_theme.background);
486 | init_pair_enhanced(26, current_theme.type, current_theme.background);
487 | init_pair_enhanced(27, current_theme.operator_color,
488 | current_theme.background);
489 | init_pair_enhanced(28, current_theme.punctuation, current_theme.background);
490 | init_pair_enhanced(29, current_theme.constant, current_theme.background);
491 | init_pair_enhanced(30, current_theme.namespace_color,
492 | current_theme.background);
493 | init_pair_enhanced(31, current_theme.property, current_theme.background);
494 | init_pair_enhanced(32, current_theme.decorator, current_theme.background);
495 | init_pair_enhanced(33, current_theme.macro, current_theme.background);
496 | init_pair_enhanced(34, current_theme.label, current_theme.background);
497 |
498 | // Markup (50-61)
499 | init_pair_enhanced(50, current_theme.markup_heading,
500 | current_theme.background);
501 | init_pair_enhanced(51, current_theme.markup_bold, current_theme.background);
502 | init_pair_enhanced(52, current_theme.markup_italic, current_theme.background);
503 | init_pair_enhanced(53, current_theme.markup_code, current_theme.background);
504 | init_pair_enhanced(54, current_theme.markup_code_block,
505 | current_theme.background);
506 | init_pair_enhanced(55, current_theme.markup_link, current_theme.background);
507 | init_pair_enhanced(56, current_theme.markup_url, current_theme.background);
508 | init_pair_enhanced(57, current_theme.markup_blockquote,
509 | current_theme.background);
510 | init_pair_enhanced(58, current_theme.markup_list, current_theme.background);
511 | init_pair_enhanced(59, current_theme.operator_color,
512 | current_theme.background);
513 | init_pair_enhanced(60, current_theme.markup_strikethrough,
514 | current_theme.background);
515 | init_pair_enhanced(61, current_theme.markup_quote, current_theme.background);
516 |
517 | // Special pairs
518 | init_pair_enhanced(70, current_theme.foreground,
519 | current_theme.line_highlight);
520 |
521 | bkgdset(' ' | COLOR_PAIR(BACKGROUND_PAIR_ID));
522 | clear();
523 | // refresh();
524 | }
525 |
526 | // Also add this helper function to detect and optimize for WSL
527 | bool StyleManager::is_wsl_environment() const
528 | {
529 | return getenv("WSL_DISTRO_NAME") != nullptr;
530 | }
531 |
532 | // Enhanced color initialization for WSL
533 | void StyleManager::optimize_for_wsl()
534 | {
535 | if (!is_wsl_environment())
536 | return;
537 |
538 | std::cerr << "WSL environment detected - applying optimizations" << std::endl;
539 |
540 | // Check Windows Terminal version and capabilities
541 | const char *wt_session = getenv("WT_SESSION");
542 | if (wt_session)
543 | {
544 | std::cerr << "Windows Terminal detected" << std::endl;
545 |
546 | // Windows Terminal supports true color
547 | if (supports_true_color())
548 | {
549 | std::cerr << "True color support detected" << std::endl;
550 | }
551 |
552 | // Force TERM to get better colors
553 | if (!getenv("TERM") || strcmp(getenv("TERM"), "xterm-256color") != 0)
554 | {
555 | std::cerr << "Setting TERM=xterm-256color for better WSL colors"
556 | << std::endl;
557 | setenv("TERM", "xterm-256color", 1);
558 | }
559 | }
560 | }
561 |
562 | // Helper function to apply color with attributes (simplified for 256-color)
563 | void StyleManager::apply_color_pair(int pair_id, ThemeColor theme_color) const
564 | {
565 | int attrs = COLOR_PAIR(pair_id) | theme_color_to_ncurses_attr(theme_color);
566 | attrset(attrs);
567 | }
568 |
569 | // Legacy compatibility functions
570 |
571 | // Terminal capability detection
572 | bool StyleManager::supports_256_colors() const { return COLORS >= 256; }
573 |
574 | bool StyleManager::supports_true_color() const
575 | {
576 | const char *colorterm = getenv("COLORTERM");
577 | return (colorterm && (std::strcmp(colorterm, "truecolor") == 0 ||
578 | std::strcmp(colorterm, "24bit") == 0));
579 | }
580 |
581 | void StyleManager::apply_legacy_theme(const Theme &theme)
582 | {
583 | if (initialized)
584 | {
585 | apply_theme();
586 | }
587 | }
588 |
589 | // YAML parsing utilities remain the same
590 | std::string StyleManager::trim(const std::string &str)
591 | {
592 | size_t start = str.find_first_not_of(" \t\n\r");
593 | if (start == std::string::npos)
594 | return "";
595 | size_t end = str.find_last_not_of(" \t\n\r");
596 | return str.substr(start, end - start + 1);
597 | }
598 |
599 | std::string StyleManager::remove_quotes(const std::string &str)
600 | {
601 | std::string trimmed = trim(str);
602 | if (trimmed.length() >= 2 &&
603 | ((trimmed.front() == '"' && trimmed.back() == '"') ||
604 | (trimmed.front() == '\'' && trimmed.back() == '\'')))
605 | {
606 | return trimmed.substr(1, trimmed.length() - 2);
607 | }
608 | return trimmed;
609 | }
610 |
611 | std::map<std::string, std::string>
612 | StyleManager::parse_yaml(const std::string &yaml_content)
613 | {
614 | std::map<std::string, std::string> result;
615 | std::istringstream stream(yaml_content);
616 | std::string line;
617 |
618 | while (std::getline(stream, line))
619 | {
620 | std::string trimmed_line = trim(line);
621 | if (trimmed_line.empty() || trimmed_line[0] == '#')
622 | continue;
623 |
624 | size_t colon_pos = line.find(':');
625 | if (colon_pos == std::string::npos)
626 | continue;
627 |
628 | std::string key = trim(line.substr(0, colon_pos));
629 | std::string value = trim(line.substr(colon_pos + 1));
630 | value = remove_quotes(value);
631 |
632 | if (!key.empty() && !value.empty())
633 | {
634 | result[key] = value;
635 | }
636 | }
637 | return result;
638 | }
639 |
640 | bool StyleManager::load_theme_from_yaml(const std::string &yaml_content)
641 | {
642 | try
643 | {
644 | auto config = parse_yaml(yaml_content);
645 | NamedTheme theme;
646 |
647 | auto get_color = [&](const std::string &key,
648 | const std::string &default_color) -> std::string
649 | {
650 | auto it = config.find(key);
651 | return (it != config.end()) ? it->second : default_color;
652 | };
653 |
654 | theme.name = config.count("name") ? config["name"] : "Custom Theme";
655 | theme.background = get_color("background", "#000000");
656 | theme.foreground = get_color("foreground", "#FFFFFF");
657 | theme.cursor = get_color("cursor", "#FFFFFF");
658 | theme.selection = get_color("selection", "#0000FF");
659 | theme.line_highlight = get_color("line_highlight", "#333333");
660 | theme.line_numbers = get_color("line_numbers", "#808080");
661 | theme.line_numbers_active = get_color("line_numbers_active", "#FFFFFF");
662 | theme.status_bar_bg = get_color("status_bar_bg", "#000080");
663 | theme.status_bar_fg = get_color("status_bar_fg", "#FFFFFF");
664 | theme.status_bar_active = get_color("status_bar_active", "#00FFFF");
665 |
666 | // Semantic categories
667 | theme.keyword = get_color("keyword", "#569CD6");
668 | theme.string_literal = get_color("string_literal", "#CE9178");
669 | theme.number = get_color("number", "#B5CEA8");
670 | theme.comment = get_color("comment", "#6A9955");
671 | theme.function_name = get_color("function_name", "#DCDCAA");
672 | theme.variable = get_color("variable", "#9CDCFE");
673 | theme.type = get_color("type", "#4EC9B0");
674 | theme.operator_color = get_color("operator", "#D4D4D4");
675 | theme.punctuation = get_color("punctuation", "#D4D4D4");
676 | theme.constant = get_color("constant", "#4FC1FF");
677 | theme.namespace_color = get_color("namespace", "#4EC9B0");
678 | theme.property = get_color("property", "#9CDCFE");
679 | theme.decorator = get_color("decorator", "#DCDCAA");
680 | theme.macro = get_color("macro", "#C586C0");
681 | theme.label = get_color("label", "#569CD6");
682 |
683 | // Markup
684 | theme.markup_heading = get_color("markup_heading", "#569CD6");
685 | theme.markup_bold = get_color("markup_bold", "#D4D4D4");
686 | theme.markup_italic = get_color("markup_italic", "#CE9178");
687 | theme.markup_code = get_color("markup_code", "#CE9178");
688 | theme.markup_code_block = get_color("markup_code_block", "#CE9178");
689 | theme.markup_link = get_color("markup_link", "#3794FF");
690 | theme.markup_url = get_color("markup_url", "#3794FF");
691 | theme.markup_list = get_color("markup_list", "#6A9955");
692 | theme.markup_blockquote = get_color("markup_blockquote", "#6A9955");
693 | theme.markup_strikethrough = get_color("markup_strikethrough", "#FF6B6B");
694 | theme.markup_quote = get_color("markup_quote", "#6A9955");
695 |
696 | current_theme = theme;
697 | if (initialized)
698 | {
699 | apply_theme();
700 | }
701 | return true;
702 | }
703 | catch (const std::exception &e)
704 | {
705 | std::cerr << "Error parsing theme: " << e.what() << std::endl;
706 | load_default_theme();
707 | return false;
708 | }
709 | }
710 |
711 | bool StyleManager::load_theme_from_file(const std::string &file_path)
712 | {
713 | try
714 | {
715 | std::ifstream file(file_path);
716 | if (!file.is_open())
717 | {
718 | std::cerr << "Failed to open theme file: " << file_path << std::endl;
719 | load_default_theme();
720 | return false;
721 | }
722 |
723 | std::stringstream buffer;
724 | buffer << file.rdbuf();
725 | file.close();
726 |
727 | return load_theme_from_yaml(buffer.str());
728 | }
729 | catch (const std::exception &e)
730 | {
731 | std::cerr << "Error reading theme file " << file_path << ": " << e.what()
732 | << std::endl;
733 | load_default_theme();
734 | return false;
735 | }
736 | }
737 |
738 | // Global instance
739 | StyleManager g_style_manager;
740 |
741 | // Legacy API functions for backward compatibility
742 | void init_colors() { g_style_manager.initialize(); }
743 |
744 | void load_default_theme()
745 | {
746 | if (!g_style_manager.is_initialized())
747 | {
748 | g_style_manager.initialize();
749 | }
750 | }
751 |
752 | void apply_theme(const Theme &theme)
753 | {
754 | g_style_manager.apply_legacy_theme(theme);
755 | }
756 |
```
--------------------------------------------------------------------------------
/deps/tree-sitter-markdown/tree-sitter-markdown/grammar.js:
--------------------------------------------------------------------------------
```javascript
1 | // This grammar only concerns the block structure according to the CommonMark Spec
2 | // (https://spec.commonmark.org/0.30/#blocks-and-inlines)
3 | // For more information see README.md
4 |
5 | /// <reference types="tree-sitter-cli/dsl" />
6 |
7 | const common = require('../common/common');
8 |
9 | const PRECEDENCE_LEVEL_LINK = common.PRECEDENCE_LEVEL_LINK;
10 |
11 | const PUNCTUATION_CHARACTERS_REGEX = '!-/:-@\\[-`\\{-~';
12 |
13 | module.exports = grammar({
14 | name: 'markdown',
15 |
16 | rules: {
17 | document: $ => seq(
18 | optional(choice(
19 | common.EXTENSION_MINUS_METADATA ? $.minus_metadata : choice(),
20 | common.EXTENSION_PLUS_METADATA ? $.plus_metadata : choice(),
21 | )),
22 | alias(prec.right(repeat($._block_not_section)), $.section),
23 | repeat($.section),
24 | ),
25 |
26 | ...common.rules,
27 | _last_token_punctuation: $ => choice(), // needed for compatability with common rules
28 |
29 | // BLOCK STRUCTURE
30 |
31 | // All blocks. Every block contains a trailing newline.
32 | _block: $ => choice(
33 | $._block_not_section,
34 | $.section,
35 | ),
36 | _block_not_section: $ => choice(
37 | alias($._setext_heading1, $.setext_heading),
38 | alias($._setext_heading2, $.setext_heading),
39 | $.paragraph,
40 | $.indented_code_block,
41 | $.block_quote,
42 | $.thematic_break,
43 | $.list,
44 | $.fenced_code_block,
45 | $._blank_line,
46 | $.html_block,
47 | $.link_reference_definition,
48 | common.EXTENSION_PIPE_TABLE ? $.pipe_table : choice(),
49 | ),
50 | section: $ => choice($._section1, $._section2, $._section3, $._section4, $._section5, $._section6),
51 | _section1: $ => prec.right(seq(
52 | alias($._atx_heading1, $.atx_heading),
53 | repeat(choice(
54 | alias(choice($._section6, $._section5, $._section4, $._section3, $._section2), $.section),
55 | $._block_not_section
56 | ))
57 | )),
58 | _section2: $ => prec.right(seq(
59 | alias($._atx_heading2, $.atx_heading),
60 | repeat(choice(
61 | alias(choice($._section6, $._section5, $._section4, $._section3), $.section),
62 | $._block_not_section
63 | ))
64 | )),
65 | _section3: $ => prec.right(seq(
66 | alias($._atx_heading3, $.atx_heading),
67 | repeat(choice(
68 | alias(choice($._section6, $._section5, $._section4), $.section),
69 | $._block_not_section
70 | ))
71 | )),
72 | _section4: $ => prec.right(seq(
73 | alias($._atx_heading4, $.atx_heading),
74 | repeat(choice(
75 | alias(choice($._section6, $._section5), $.section),
76 | $._block_not_section
77 | ))
78 | )),
79 | _section5: $ => prec.right(seq(
80 | alias($._atx_heading5, $.atx_heading),
81 | repeat(choice(
82 | alias($._section6, $.section),
83 | $._block_not_section
84 | ))
85 | )),
86 | _section6: $ => prec.right(seq(
87 | alias($._atx_heading6, $.atx_heading),
88 | repeat($._block_not_section)
89 | )),
90 |
91 | // LEAF BLOCKS
92 |
93 | // A thematic break. This is currently handled by the external scanner but maybe could be
94 | // parsed using normal tree-sitter rules.
95 | //
96 | // https://github.github.com/gfm/#thematic-breaks
97 | thematic_break: $ => seq($._thematic_break, choice($._newline, $._eof)),
98 |
99 | // An ATX heading. This is currently handled by the external scanner but maybe could be
100 | // parsed using normal tree-sitter rules.
101 | //
102 | // https://github.github.com/gfm/#atx-headings
103 | _atx_heading1: $ => prec(1, seq(
104 | $.atx_h1_marker,
105 | optional($._atx_heading_content),
106 | $._newline
107 | )),
108 | _atx_heading2: $ => prec(1, seq(
109 | $.atx_h2_marker,
110 | optional($._atx_heading_content),
111 | $._newline
112 | )),
113 | _atx_heading3: $ => prec(1, seq(
114 | $.atx_h3_marker,
115 | optional($._atx_heading_content),
116 | $._newline
117 | )),
118 | _atx_heading4: $ => prec(1, seq(
119 | $.atx_h4_marker,
120 | optional($._atx_heading_content),
121 | $._newline
122 | )),
123 | _atx_heading5: $ => prec(1, seq(
124 | $.atx_h5_marker,
125 | optional($._atx_heading_content),
126 | $._newline
127 | )),
128 | _atx_heading6: $ => prec(1, seq(
129 | $.atx_h6_marker,
130 | optional($._atx_heading_content),
131 | $._newline
132 | )),
133 | _atx_heading_content: $ => prec(1, seq(
134 | optional($._whitespace),
135 | field('heading_content', alias($._line, $.inline))
136 | )),
137 |
138 | // A setext heading. The underlines are currently handled by the external scanner but maybe
139 | // could be parsed using normal tree-sitter rules.
140 | //
141 | // https://github.github.com/gfm/#setext-headings
142 | _setext_heading1: $ => seq(
143 | field('heading_content', $.paragraph),
144 | $.setext_h1_underline,
145 | choice($._newline, $._eof),
146 | ),
147 | _setext_heading2: $ => seq(
148 | field('heading_content', $.paragraph),
149 | $.setext_h2_underline,
150 | choice($._newline, $._eof),
151 | ),
152 |
153 | // An indented code block. An indented code block is made up of indented chunks and blank
154 | // lines. The indented chunks are handeled by the external scanner.
155 | //
156 | // https://github.github.com/gfm/#indented-code-blocks
157 | indented_code_block: $ => prec.right(seq($._indented_chunk, repeat(choice($._indented_chunk, $._blank_line)))),
158 | _indented_chunk: $ => seq($._indented_chunk_start, repeat(choice($._line, $._newline)), $._block_close, optional($.block_continuation)),
159 |
160 | // A fenced code block. Fenced code blocks are mainly handled by the external scanner. In
161 | // case of backtick code blocks the external scanner also checks that the info string is
162 | // proper.
163 | //
164 | // https://github.github.com/gfm/#fenced-code-blocks
165 | fenced_code_block: $ => prec.right(choice(
166 | seq(
167 | alias($._fenced_code_block_start_backtick, $.fenced_code_block_delimiter),
168 | optional($._whitespace),
169 | optional($.info_string),
170 | $._newline,
171 | optional($.code_fence_content),
172 | optional(seq(alias($._fenced_code_block_end_backtick, $.fenced_code_block_delimiter), $._close_block, $._newline)),
173 | $._block_close,
174 | ),
175 | seq(
176 | alias($._fenced_code_block_start_tilde, $.fenced_code_block_delimiter),
177 | optional($._whitespace),
178 | optional($.info_string),
179 | $._newline,
180 | optional($.code_fence_content),
181 | optional(seq(alias($._fenced_code_block_end_tilde, $.fenced_code_block_delimiter), $._close_block, $._newline)),
182 | $._block_close,
183 | ),
184 | )),
185 | code_fence_content: $ => repeat1(choice($._newline, $._line)),
186 | info_string: $ => choice(
187 | seq($.language, repeat(choice($._line, $.backslash_escape, $.entity_reference, $.numeric_character_reference))),
188 | seq(
189 | repeat1(choice('{', '}')),
190 | optional(choice(
191 | seq($.language, repeat(choice($._line, $.backslash_escape, $.entity_reference, $.numeric_character_reference))),
192 | seq($._whitespace, repeat(choice($._line, $.backslash_escape, $.entity_reference, $.numeric_character_reference))),
193 | ))
194 | )
195 | ),
196 | language: $ => prec.right(repeat1(choice($._word, common.punctuation_without($, ['{', '}', ',']), $.backslash_escape, $.entity_reference, $.numeric_character_reference))),
197 |
198 | // An HTML block. We do not emit addition nodes relating to the kind or structure or of the
199 | // html block as this is best done using language injections and a proper html parsers.
200 | //
201 | // See the `build_html_block` function for more information.
202 | // See the spec for the different kinds of html blocks.
203 | //
204 | // https://github.github.com/gfm/#html-blocks
205 | html_block: $ => prec(1, seq(optional($._whitespace), choice(
206 | $._html_block_1,
207 | $._html_block_2,
208 | $._html_block_3,
209 | $._html_block_4,
210 | $._html_block_5,
211 | $._html_block_6,
212 | $._html_block_7,
213 | ))),
214 | _html_block_1: $ => build_html_block($,
215 | // new RegExp(
216 | // '[ \t]*<' + regex_case_insensitive_list(HTML_TAG_NAMES_RULE_1) + '([\\r\\n]|[ \\t>][^<\\r\\n]*(\\n|\\r\\n?)?)'
217 | // ),
218 | $._html_block_1_start,
219 | $._html_block_1_end,
220 | true
221 | ),
222 | _html_block_2: $ => build_html_block($, $._html_block_2_start, '-->', true),
223 | _html_block_3: $ => build_html_block($, $._html_block_3_start, '?>', true),
224 | _html_block_4: $ => build_html_block($, $._html_block_4_start, '>', true),
225 | _html_block_5: $ => build_html_block($, $._html_block_5_start, ']]>', true),
226 | _html_block_6: $ => build_html_block(
227 | $,
228 | $._html_block_6_start,
229 | seq($._newline, $._blank_line),
230 | true
231 | ),
232 | _html_block_7: $ => build_html_block(
233 | $,
234 | $._html_block_7_start,
235 | seq($._newline, $._blank_line),
236 | false
237 | ),
238 |
239 | // A link reference definition. We need to make sure that this is not mistaken for a
240 | // paragraph or indented chunk. The `$._no_indented_chunk` token is used to tell the
241 | // external scanner not to allow indented chunks when the `$.link_title` of the link
242 | // reference definition would be valid.
243 | //
244 | // https://github.github.com/gfm/#link-reference-definitions
245 | link_reference_definition: $ => prec.dynamic(PRECEDENCE_LEVEL_LINK, seq(
246 | optional($._whitespace),
247 | $.link_label,
248 | ':',
249 | optional(seq(optional($._whitespace), optional(seq($._soft_line_break, optional($._whitespace))))),
250 | $.link_destination,
251 | optional(prec.dynamic(2 * PRECEDENCE_LEVEL_LINK, seq(
252 | choice(
253 | seq($._whitespace, optional(seq($._soft_line_break, optional($._whitespace)))),
254 | seq($._soft_line_break, optional($._whitespace)),
255 | ),
256 | optional($._no_indented_chunk),
257 | $.link_title
258 | ))),
259 | choice($._newline, $._soft_line_break, $._eof),
260 | )),
261 | _text_inline_no_link: $ => choice($._word, $._whitespace, common.punctuation_without($, ['[', ']'])),
262 |
263 | // A paragraph. The parsing tactic for deciding when a paragraph ends is as follows:
264 | // on every newline inside a paragraph a conflict is triggered manually using
265 | // `$._split_token` to split the parse state into two branches.
266 | //
267 | // One of them - the one that also contains a `$._soft_line_break_marker` will try to
268 | // continue the paragraph, but we make sure that the beginning of a new block that can
269 | // interrupt a paragraph can also be parsed. If this is the case we know that the paragraph
270 | // should have been closed and the external parser will emit an `$._error` to kill the parse
271 | // branch.
272 | //
273 | // The other parse branch consideres the paragraph to be over. It will be killed if no valid new
274 | // block is detected before the next newline. (For example it will also be killed if a indented
275 | // code block is detected, which cannot interrupt paragraphs).
276 | //
277 | // Either way, after the next newline only one branch will exist, so the ammount of branches
278 | // related to paragraphs ending does not grow.
279 | //
280 | // https://github.github.com/gfm/#paragraphs
281 | paragraph: $ => seq(alias(repeat1(choice($._line, $._soft_line_break)), $.inline), choice($._newline, $._eof)),
282 |
283 | // A blank line including the following newline.
284 | //
285 | // https://github.github.com/gfm/#blank-lines
286 | _blank_line: $ => seq($._blank_line_start, choice($._newline, $._eof)),
287 |
288 |
289 | // CONTAINER BLOCKS
290 |
291 | // A block quote. This is the most basic example of a container block handled by the
292 | // external scanner.
293 | //
294 | // https://github.github.com/gfm/#block-quotes
295 | block_quote: $ => seq(
296 | alias($._block_quote_start, $.block_quote_marker),
297 | optional($.block_continuation),
298 | repeat($._block),
299 | $._block_close,
300 | optional($.block_continuation)
301 | ),
302 |
303 | // A list. This grammar does not differentiate between loose and tight lists for efficiency
304 | // reasons.
305 | //
306 | // Lists can only contain list items with list markers of the same type. List items are
307 | // handled by the external scanner.
308 | //
309 | // https://github.github.com/gfm/#lists
310 | list: $ => prec.right(choice(
311 | $._list_plus,
312 | $._list_minus,
313 | $._list_star,
314 | $._list_dot,
315 | $._list_parenthesis
316 | )),
317 | _list_plus: $ => prec.right(repeat1(alias($._list_item_plus, $.list_item))),
318 | _list_minus: $ => prec.right(repeat1(alias($._list_item_minus, $.list_item))),
319 | _list_star: $ => prec.right(repeat1(alias($._list_item_star, $.list_item))),
320 | _list_dot: $ => prec.right(repeat1(alias($._list_item_dot, $.list_item))),
321 | _list_parenthesis: $ => prec.right(repeat1(alias($._list_item_parenthesis, $.list_item))),
322 | // Some list items can not interrupt a paragraph and are marked as such by the external
323 | // scanner.
324 | list_marker_plus: $ => choice($._list_marker_plus, $._list_marker_plus_dont_interrupt),
325 | list_marker_minus: $ => choice($._list_marker_minus, $._list_marker_minus_dont_interrupt),
326 | list_marker_star: $ => choice($._list_marker_star, $._list_marker_star_dont_interrupt),
327 | list_marker_dot: $ => choice($._list_marker_dot, $._list_marker_dot_dont_interrupt),
328 | list_marker_parenthesis: $ => choice($._list_marker_parenthesis, $._list_marker_parenthesis_dont_interrupt),
329 | _list_item_plus: $ => seq(
330 | $.list_marker_plus,
331 | optional($.block_continuation),
332 | $._list_item_content,
333 | $._block_close,
334 | optional($.block_continuation)
335 | ),
336 | _list_item_minus: $ => seq(
337 | $.list_marker_minus,
338 | optional($.block_continuation),
339 | $._list_item_content,
340 | $._block_close,
341 | optional($.block_continuation)
342 | ),
343 | _list_item_star: $ => seq(
344 | $.list_marker_star,
345 | optional($.block_continuation),
346 | $._list_item_content,
347 | $._block_close,
348 | optional($.block_continuation)
349 | ),
350 | _list_item_dot: $ => seq(
351 | $.list_marker_dot,
352 | optional($.block_continuation),
353 | $._list_item_content,
354 | $._block_close,
355 | optional($.block_continuation)
356 | ),
357 | _list_item_parenthesis: $ => seq(
358 | $.list_marker_parenthesis,
359 | optional($.block_continuation),
360 | $._list_item_content,
361 | $._block_close,
362 | optional($.block_continuation)
363 | ),
364 | // List items are closed after two consecutive blank lines
365 | _list_item_content: $ => choice(
366 | prec(1, seq(
367 | $._blank_line,
368 | $._blank_line,
369 | $._close_block,
370 | optional($.block_continuation)
371 | )),
372 | repeat1($._block),
373 | common.EXTENSION_TASK_LIST ? prec(1, seq(
374 | choice($.task_list_marker_checked, $.task_list_marker_unchecked),
375 | $._whitespace,
376 | $.paragraph,
377 | repeat($._block)
378 | )) : choice()
379 | ),
380 |
381 | // Newlines as in the spec. Parsing a newline triggers the matching process by making
382 | // the external parser emit a `$._line_ending`.
383 | _newline: $ => seq(
384 | $._line_ending,
385 | optional($.block_continuation)
386 | ),
387 | _soft_line_break: $ => seq(
388 | $._soft_line_ending,
389 | optional($.block_continuation)
390 | ),
391 | // Some symbols get parsed as single tokens so that html blocks get detected properly
392 | _line: $ => prec.right(repeat1(choice($._word, $._whitespace, common.punctuation_without($, [])))),
393 | _word: $ => choice(
394 | new RegExp('[^' + PUNCTUATION_CHARACTERS_REGEX + ' \\t\\n\\r]+'),
395 | common.EXTENSION_TASK_LIST ? choice(
396 | /\[[xX]\]/,
397 | /\[[ \t]\]/,
398 | ) : choice()
399 | ),
400 | // The external scanner emits some characters that should just be ignored.
401 | _whitespace: $ => /[ \t]+/,
402 |
403 | ...(common.EXTENSION_TASK_LIST ? {
404 | task_list_marker_checked: $ => prec(1, /\[[xX]\]/),
405 | task_list_marker_unchecked: $ => prec(1, /\[[ \t]\]/),
406 | } : {}),
407 |
408 | ...(common.EXTENSION_PIPE_TABLE ? {
409 | pipe_table: $ => prec.right(seq(
410 | $._pipe_table_start,
411 | alias($.pipe_table_row, $.pipe_table_header),
412 | $._newline,
413 | $.pipe_table_delimiter_row,
414 | repeat(seq($._pipe_table_newline, optional($.pipe_table_row))),
415 | choice($._newline, $._eof),
416 | )),
417 |
418 | _pipe_table_newline: $ => seq(
419 | $._pipe_table_line_ending,
420 | optional($.block_continuation)
421 | ),
422 |
423 | pipe_table_delimiter_row: $ => seq(
424 | optional(seq(
425 | optional($._whitespace),
426 | '|',
427 | )),
428 | repeat1(prec.right(seq(
429 | optional($._whitespace),
430 | $.pipe_table_delimiter_cell,
431 | optional($._whitespace),
432 | '|',
433 | ))),
434 | optional($._whitespace),
435 | optional(seq(
436 | $.pipe_table_delimiter_cell,
437 | optional($._whitespace)
438 | )),
439 | ),
440 |
441 | pipe_table_delimiter_cell: $ => seq(
442 | optional(alias(':', $.pipe_table_align_left)),
443 | repeat1('-'),
444 | optional(alias(':', $.pipe_table_align_right)),
445 | ),
446 |
447 | pipe_table_row: $ => seq(
448 | optional(seq(
449 | optional($._whitespace),
450 | '|',
451 | )),
452 | choice(
453 | seq(
454 | repeat1(prec.right(seq(
455 | choice(
456 | seq(
457 | optional($._whitespace),
458 | $.pipe_table_cell,
459 | optional($._whitespace)
460 | ),
461 | alias($._whitespace, $.pipe_table_cell)
462 | ),
463 | '|',
464 | ))),
465 | optional($._whitespace),
466 | optional(seq(
467 | $.pipe_table_cell,
468 | optional($._whitespace)
469 | )),
470 | ),
471 | seq(
472 | optional($._whitespace),
473 | $.pipe_table_cell,
474 | optional($._whitespace)
475 | )
476 | ),
477 | ),
478 |
479 | pipe_table_cell: $ => prec.right(seq(
480 | choice(
481 | $._word,
482 | $._backslash_escape,
483 | common.punctuation_without($, ['|']),
484 | ),
485 | repeat(choice(
486 | $._word,
487 | $._whitespace,
488 | $._backslash_escape,
489 | common.punctuation_without($, ['|']),
490 | )),
491 | )),
492 | } : {}),
493 | },
494 |
495 | externals: $ => [
496 | // Quite a few of these tokens could maybe be implemented without use of the external parser.
497 | // For this the `$._open_block` and `$._close_block` tokens should be used to tell the external
498 | // parser to put a new anonymous block on the block stack.
499 |
500 | // Block structure gets parsed as follows: After every newline (`$._line_ending`) we try to match
501 | // as many open blocks as possible. For example if the last line was part of a block quote we look
502 | // for a `>` at the beginning of the next line. We emit a `$.block_continuation` for each matched
503 | // block. For this process the external scanner keeps a stack of currently open blocks.
504 | //
505 | // If we are not able to match all blocks that does not necessarily mean that all unmatched blocks
506 | // have to be closed. It could also mean that the line is a lazy continuation line
507 | // (https://github.github.com/gfm/#lazy-continuation-line, see also `$._split_token` and
508 | // `$._soft_line_break_marker` below)
509 | //
510 | // If a block does get closed (because it was not matched or because some closing token was
511 | // encountered) we emit a `$._block_close` token
512 |
513 | $._line_ending, // this token does not contain the actual newline characters. see `$._newline`
514 | $._soft_line_ending,
515 | $._block_close,
516 | $.block_continuation,
517 |
518 | // Tokens signifying the start of a block. Blocks that do not need a `$._block_close` because they
519 | // always span one line are marked as such.
520 |
521 | $._block_quote_start,
522 | $._indented_chunk_start,
523 | $.atx_h1_marker, // atx headings do not need a `$._block_close`
524 | $.atx_h2_marker,
525 | $.atx_h3_marker,
526 | $.atx_h4_marker,
527 | $.atx_h5_marker,
528 | $.atx_h6_marker,
529 | $.setext_h1_underline, // setext headings do not need a `$._block_close`
530 | $.setext_h2_underline,
531 | $._thematic_break, // thematic breaks do not need a `$._block_close`
532 | $._list_marker_minus,
533 | $._list_marker_plus,
534 | $._list_marker_star,
535 | $._list_marker_parenthesis,
536 | $._list_marker_dot,
537 | $._list_marker_minus_dont_interrupt, // list items that do not interrupt an ongoing paragraph
538 | $._list_marker_plus_dont_interrupt,
539 | $._list_marker_star_dont_interrupt,
540 | $._list_marker_parenthesis_dont_interrupt,
541 | $._list_marker_dot_dont_interrupt,
542 | $._fenced_code_block_start_backtick,
543 | $._fenced_code_block_start_tilde,
544 | $._blank_line_start, // Does not contain the newline characters. Blank lines do not need a `$._block_close`
545 |
546 | // Special tokens for block structure
547 |
548 | // Closing backticks or tildas for a fenced code block. They are used to trigger a `$._close_block`
549 | // which in turn will trigger a `$._block_close` at the beginning the following line.
550 | $._fenced_code_block_end_backtick,
551 | $._fenced_code_block_end_tilde,
552 |
553 | $._html_block_1_start,
554 | $._html_block_1_end,
555 | $._html_block_2_start,
556 | $._html_block_3_start,
557 | $._html_block_4_start,
558 | $._html_block_5_start,
559 | $._html_block_6_start,
560 | $._html_block_7_start,
561 |
562 | // Similarly this is used if the closing of a block is not decided by the external parser.
563 | // A `$._block_close` will be emitted at the beginning of the next line. Notice that a
564 | // `$._block_close` can also get emitted if the parent block closes.
565 | $._close_block,
566 |
567 | // This is a workaround so the external parser does not try to open indented blocks when
568 | // parsing a link reference definition.
569 | $._no_indented_chunk,
570 |
571 | // An `$._error` token is never valid and gets emmited to kill invalid parse branches. Concretely
572 | // this is used to decide wether a newline closes a paragraph and together and it gets emitted
573 | // when trying to parse the `$._trigger_error` token in `$.link_title`.
574 | $._error,
575 | $._trigger_error,
576 | $._eof,
577 |
578 | $.minus_metadata,
579 | $.plus_metadata,
580 |
581 | $._pipe_table_start,
582 | $._pipe_table_line_ending,
583 | ],
584 | precedences: $ => [
585 | [$._setext_heading1, $._block],
586 | [$._setext_heading2, $._block],
587 | [$.indented_code_block, $._block],
588 | ],
589 | conflicts: $ => [
590 | [$.link_reference_definition],
591 | [$.link_label, $._line],
592 | [$.link_reference_definition, $._line],
593 | ],
594 | extras: $ => [],
595 | });
596 |
597 | // General purpose structure for html blocks. The different kinds mostly work the same but have
598 | // different openling and closing conditions. Some html blocks may not interrupt a paragraph and
599 | // have to be marked as such.
600 | function build_html_block($, open, close, interrupt_paragraph) {
601 | return seq(
602 | open,
603 | repeat(choice(
604 | $._line,
605 | $._newline,
606 | seq(close, $._close_block),
607 | )),
608 | $._block_close,
609 | optional($.block_continuation),
610 | );
611 | }
612 |
```