diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index 02f7865..ae4de23 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -25,6 +25,7 @@ runs: sparse-checkout: | lib/ test/ + example/ analysis_options.yaml pubspec.yaml README.md diff --git a/.github/workflows/checkout.yml b/.github/workflows/checkout.yml index 337807f..d840591 100644 --- a/.github/workflows/checkout.yml +++ b/.github/workflows/checkout.yml @@ -104,6 +104,14 @@ jobs: run: | flutter test --coverage --concurrency=40 test/unit_test.dart + - name: ๐Ÿงช Run example tests + id: run-example-tests + timeout-minutes: 5 + working-directory: example + run: | + flutter pub get + flutter test + # - name: ๐Ÿ“Š Upload coverage to Codecov # id: upload-coverage # timeout-minutes: 2 diff --git a/.gitignore b/.gitignore index ccf9a29..39aec15 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ pubspec.lock *.exe # Benchmark comparison baseline (machine-specific, generated by benchmark/compare.dart --save) benchmark/.baseline.txt +benchmark/.render_baseline.txt +# Node tooling for the syntax-highlight codegen (tool/highlight_codegen); +# grammars.json is a regenerable snapshot, languages.json is the source of truth. +node_modules/ +tool/highlight_codegen/package-lock.json +tool/highlight_codegen/grammars.json +*.err diff --git a/.pubignore b/.pubignore index 80bfe48..11527de 100644 --- a/.pubignore +++ b/.pubignore @@ -1,5 +1,22 @@ +.dart_tool/ +pubspec.lock +benchmark/.baseline.txt +benchmark/.render_baseline.txt .vscode/ build/ coverage/ credentials.json -*.exe \ No newline at end of file +*.exe +benchmark/ +benchmark_compare/ +example/lib/experiments/ +example/android/ +example/ios/ +example/windows/ +example/macos/ +example/linux/ +docs/ +# Prism codegen tooling โ€” not part of the published package. (.pubignore does +# not inherit .gitignore, so node_modules must be excluded explicitly here.) +tool/ +node_modules/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8644471 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,110 @@ +# AGENTS.md + +High-signal orientation for LLMs/agents working in **`flutter_md`**. Read this +first, every time. Deep detail lives in [`docs/`](docs/) โ€” linked per section. + +`flutter_md` is a Flutter Markdown package: a hand-rolled **parser** โ†’ an +immutable **node model** โ†’ a **canvas render layer** (one `RenderBox`, no +widget-per-block) โ†’ cross-block/cross-widget **text selection**. Repo: +`DoctorinaAI/md`, package `flutter_md`, currently `0.2.0`, branch +`feat/text-selection`. + +## Commands (these are the CI gates โ€” run before you claim done) + +```shell +# Tests โ€” test/unit_test.dart is the single aggregate entrypoint CI runs. +flutter test test/unit_test.dart +flutter test --coverage --concurrency=40 test/unit_test.dart # CI form + +# Analyzer โ€” INFO-level lints FAIL (--fatal-infos). A missing doc comment fails CI. +dart analyze --fatal-infos --fatal-warnings lib/ test/ + +# Format โ€” 80 columns, strict, exactly as CI checks it: +find lib test -name "*.dart" ! -name "*.*.dart" -print0 \ + | xargs -0 dart format --set-exit-if-changed --line-length 80 -o none +# to actually format: dart format lib test example (analysis_options pins page_width: 80) +``` + +Benchmarks and the example app: see [`docs/development.md`](docs/development.md). +The render benchmark runs under `flutter test` (needs `dart:ui`); parser +benchmarks run under `dart run`. + +## Module map (`lib/src/`) + +| Path | What | Doc | +|---|---|---| +| `parser.dart` | `MarkdownDecoder` (a `Converter`); one hand-rolled line loop, perf-tuned | [parser](docs/parser.md) | +| `nodes.dart` | `MD$*` immutable node tree, `MD$Style` bitmask, `.map()` dispatch | [parser](docs/parser.md) | +| `markdown.dart` | `Markdown` model + `Markdown.fromString(...)` entry point | [parser](docs/parser.md) | +| `theme.dart` | `MarkdownThemeData` (a `ThemeExtension`), `MarkdownTheme`; `builder`/`blockFilter`/`spanFilter` hooks | [rendering](docs/rendering.md) | +| `render.dart` | **re-export barrel** for `render/` (keeps `src/render.dart` imports working) | [rendering](docs/rendering.md) | +| `render/block_painter.dart` | `BlockPainter` framework: interfaces + mixins (`SelectableTextBlock`, `MultiPainterSelectable`, `ParagraphGestureHandler`, `SelectableFragment`) | [rendering](docs/rendering.md) | +| `render/span_builder.dart` | `paragraphFromMarkdownSpans(...)` โ€” the public spanโ†’`TextSpan` helper | [rendering](docs/rendering.md) | +| `render/markdown_painter.dart` | `MarkdownPainter` orchestrator (`@meta.internal`): block list, layout, cached `ui.Picture`, hit-test | [rendering](docs/rendering.md) | +| `render/markdown_render_object.dart` | `MarkdownRenderObject` (`@meta.internal`) โ€” the `RenderBox`, also a `MarkdownSelectionSurface` | [rendering](docs/rendering.md) | +| `render/blocks/*.dart` | `BlockPainter$Paragraph โ€ฆ $Table` โ€” the 9 default painters | [rendering](docs/rendering.md) | +| `selection.dart` | `MarkdownSelectionController`, `MarkdownPosition/Selection`, registry, reconciliation, formatters, `markdownBlockRenderedText` | [selection](docs/selection.md) | +| `selection_scope.dart` | `MarkdownSelectionScope` โ€” gestures, keyboard, handles, toolbar; `MarkdownSelectionGroup` | [selection](docs/selection.md) | +| `widget.dart` | `MarkdownWidget` (`LeafRenderObjectWidget`) โ€” the public entry widget | [rendering](docs/rendering.md) | + +Public API is the barrel `lib/flutter_md.dart` (`export โ€ฆ show โ€ฆ`). See +[`docs/architecture.md`](docs/architecture.md) for the full data flow. + +## Hard rules (violating these breaks CI or the architecture) + +1. **80-col + `--fatal-infos`.** No line > 80 chars. Every public member needs a + `///` doc (`public_member_api_docs: true`). Infos are fatal in CI. +2. **Imports:** relative inside `lib/` (`../nodes.dart`), `package:flutter_md/โ€ฆ` + in `test/`. `prefer_relative_imports` + `avoid_relative_lib_imports` enforce this. +3. **`$` is the public naming convention** for variant families: `MD$Block`, + `MD$Span`, `BlockPainter$Paragraph`. Not a typo โ€” keep it. +4. **`@meta.internal`** marks non-user-facing types (`MarkdownPainter`, + `MarkdownRenderObject`). They are reachable only via `src/render.dart`, never + in the `flutter_md.dart` `show` list. Everything else in the `show` list is + supported public API โ€” treat additions as permanent. +5. **One test entrypoint:** a new `test/**/foo_test.dart` must be wired into + `test/unit_test.dart` (import + `main()` inside `group('Unit', โ€ฆ)`) or CI won't run it. +6. **CHANGELOG discipline:** the `version:` in `pubspec.yaml` must have a matching + `# ` heading in `CHANGELOG.md` or the CI setup step fails. + +## Load-bearing invariants (don't break silently) + +- **Render is canvas-painter based**, not widget-per-block. One `MarkdownPainter` + holds a `List`; blocks stack vertically (no implicit gaps โ€” a + `MD$Spacer` supplies them); block hit-testing is a binary search over + `_blockOffsets` by `dy`. +- **Glyphs are cached in a `ui.Picture` keyed by size.** It is reused on repaint + and only invalidated by `update`/`invalidateLayout`. Do not route selection or + scroll repaints through it. +- **Selection highlight is painted OUTSIDE that cached Picture**, beneath the + glyphs. This is why a drag/streaming update never rebuilds the glyph cache and + why `isRepaintBoundary => controller != null`. Preserve this if you touch paint. +- **Selection is controller-anchored on immutable models**, not on render objects, + as `(documentId, blockIndex, renderedOffset)`. So selected text survives + `ListView` disposal (scrolled-off chat messages). A block's on-screen offset + space **must** match `markdownBlockRenderedText(block)` (lists join items with + `\n`, tables join cells with `\t` / rows with `\n`) โ€” hit-testing, highlight, + and copied text all depend on that agreement. +- **The span offset invariant:** concatenating a block's `MD$Span.text` reproduces + its rendered text; `MD$Span.start/end` index that visible text (see caveats for + escapes/math/links in [`docs/parser.md`](docs/parser.md)). Selection relies on it. + +## Gotchas quick-reference + +- `MarkdownThemeData.spanFilter` dropping **text-bearing** spans desyncs selection + offsets (highlight stays right, copied text drifts). Avoid with selection on. +- `MarkdownSelectionController.selectionColor` setter repaints surfaces directly + and must **not** `notifyListeners` (it's applied during build โ†’ would `setState`). +- Alert **title** is not selectable (body only). Keyboard word/line extension is + block-approximate. `MarkdownSelectedBlock.sourceRange` is currently always null. +- `__x__` = **underline**, not bold. Soft line breaks are preserved inside + paragraphs. Inline `$โ€ฆ$` math is **opt-in** (`inlineMath: true`). +- `example/` is a separate package (`md_example`, `path: ../`). + +## The docs + +- [`docs/architecture.md`](docs/architecture.md) โ€” modules, data flow, invariants, public API surface. +- [`docs/parser.md`](docs/parser.md) โ€” parser pipeline, node model, `MD$Style`, GFM + nonstandard choices, offset invariant. +- [`docs/rendering.md`](docs/rendering.md) โ€” render flow, the `BlockPainter` framework, **writing a custom block painter**, theme customization. +- [`docs/selection.md`](docs/selection.md) โ€” controller-anchored selection, registry, surfaces, reconciliation, extraction, the scope widget. +- [`docs/development.md`](docs/development.md) โ€” commands, CI pipeline, lint rules, conventions, benchmarks, layout. diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f4b1d..0402849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,113 @@ +## 0.2.0 + +> **Upgrading from 0.0.x?** See the +> [migration guide](docs/migration/0.0.x-to-0.2.x.md). 0.2.x is almost entirely +> backward compatible โ€” the only required code change is a new `alert` branch +> for direct `MD$Block.map` / `switch` callers. + +- **ADDED**: Opt-in, dependency-free syntax highlighting for fenced code blocks + (65+ languages, GitHub light/dark themes). Assign a `SyntaxHighlighter` to the + new `MarkdownThemeData.highlighter` field; the default (unset) renders code as + plain monospace, so existing usage is unchanged. New public API on + `package:flutter_md/highlight.dart`: `SyntaxHighlighter`, `MarkdownHighlighter`, + `CodeHighlightTheme`, `Grammar`, `GrammarToken`, `compileHighlightPattern` + (`SyntaxHighlighter` / `CodeHighlightTheme` are also re-exported from the main + entrypoint). Each language is its own library + (`package:flutter_md/highlight/.dart`, e.g. `HighlightDart.grammar`) with + no central registry, so importing one never references the others and unused + grammars tree-shake away โ€” a Dart-only app adds ~0 beyond the engine; all 65 + add ~62 KB gzipped. `HighlightThemes.githubDark` / `githubLight` + (`highlight/themes.dart`) provide ready themes; `allHighlightLanguages` + (`highlight/all.dart`) is a convenience registry of every grammar for + demos/tooling (it references all languages, so unused ones can no longer + tree-shake away). The highlighter only partitions text โ€” never edits it โ€” so + selection and copy stay aligned. Grammars are generated by + `tool/highlight_codegen` (adapted from Prism, MIT). +- **ADDED**: Cross-block and cross-widget text selection. A + `MarkdownSelectionController` anchors the selection on the immutable model, so + it spans multiple blocks and multiple `MarkdownWidget`s and survives list + disposal (e.g. chat scrolling). New public API: `MarkdownSelectionController`, + `MarkdownSelectionScope`, `MarkdownSelectionGroup`, `MarkdownPosition`, + `MarkdownSelection`, `MarkdownDocumentRef`, `MarkdownSelectedContent` + (+ document/block), `MarkdownSelectionFormatter` / + `MarkdownPlainTextFormatter` / `MarkdownMarkupFormatter`, + `MarkdownReconciliationPolicy`, `MarkdownSelectionSurface`, + `markdownBlockRenderedText`, and + `SelectableBlockPainter` / `SelectableTextBlock`. +- **ADDED**: `StreamingMarkdownParser`, an incremental parser for streaming + sources such as LLM token output. It freezes completed blocks (a block ends at + a blank line, outside any open code fence) so only the still-growing tail is + re-parsed as tokens arrive โ€” turning the `O(Nยฒ)` cost of re-parsing the whole + buffer on every token into roughly `O(tail)` (3โ€“14ร— faster on a full message + stream in `benchmark/streaming_benchmark.dart`). `parser.add(chunk)` returns + the growing `Markdown`, always identical block-for-block to + `Markdown.fromString(everythingSoFar)`, and a `Stream.toMarkdown()` + extension wires it into a stream transform. Pass a configured `MarkdownDecoder` + (e.g. `inlineMath: true`) to match `Markdown.fromString`. The batch + `MarkdownDecoder` hot path is byte-for-byte unchanged. +- **ADDED**: `MarkdownMarkupFormatter`, a built-in "Copy as Markdown" formatter. + Pass it to `getText()` (or set `controller.formatter`) to reconstruct Markdown + structure on copy โ€” heading `#`s, nested list markers with task checkboxes, + blockquote/alert `>` prefixes, fenced code and pipe tables โ€” for blocks the + selection covers in full; partially-selected boundary blocks fall back to the + plain sliced text so nothing outside the selection is emitted. The default + copy behaviour is unchanged (`MarkdownPlainTextFormatter`). +- **ADDED**: `MarkdownWidget` gains optional `documentId` and `controller` + parameters (resolved from the ambient scope). Backward compatible: a widget + with no `documentId` is inert. +- **ADDED**: Lists and tables are now interactively selectable. A new + `MultiPainterSelectable` mixin (+ `SelectableFragment`) maps pointer positions + and highlight boxes across the many `TextPainter`s of a list's items or a + table's cells, so a drag can start or end inside a list item or table cell and + the copied text keeps the `\n` / `\t` separators of `markdownBlockRenderedText`. +- **ADDED**: Keyboard shortcuts and a context toolbar on `MarkdownSelectionScope`, + mirroring `SelectableRegion`/`SelectableText`. When focused: `Ctrl/Cmd+C` + copies, `Ctrl/Cmd+A` selects all, `Shift`+arrows extend by character / word / + line / document (and vertically by geometry), `Esc` clears. Right-click + (desktop) / long-press (mobile) shows an adaptive Copy / Select-all toolbar. + The scope is now a `StatefulWidget` with a public `MarkdownSelectionScopeState` + (`copySelection` / `selectAll` / `clearSelection` / `showToolbar` / + `hideToolbar` / `contextMenuButtonItems` / `contextMenuAnchors`). New + customization params: `focusNode`, `enabled`, `selectionColor`, + `contextMenuBuilder`, `magnifierConfiguration`, `selectionControls`, + `onSelectionChanged`. New controller ops: `selectionColor`, + `globalSelectionRects`, `moveSelectionEdgeToGlobal`, and the + `extendSelectionBy*` family; `MarkdownPosition.copyWith`. +- **ADDED**: Native selection handles and a magnifier on touch platforms, + driven by Flutter's `SelectionOverlay`. Selection endpoints push + `LeaderLayer`s from the render objects so the handles follow the content as it + scrolls (and across multiple `MarkdownWidget`s); dragging a handle adjusts the + selection and shows the platform magnifier. Handles/magnifier respect the + platform (`selectionControls`, `magnifierConfiguration`) and are absent on + desktop, matching `SelectableText`. New surface geometry: + `localSelectionRects`, `setSelectionHandleLayers`, `repaintSelection`, and + `MarkdownSelectionController.selectionHandleEndpoints` / + `MarkdownHandleEndpoints`. +- **ADDED**: Word- and block-granular selection gestures. Double-click/tap + selects the word under the pointer, triple-click/tap selects the whole block, + a single click collapses (clears) the selection, and `Shift`-click extends it. + Dragging after a double/triple click keeps word/block granularity; a touch + long-press grabs the whole word (then extends by word), and a touch + double-tap selects the word and pops the toolbar. Word boundaries use the + platform word segmentation (`TextPainter.getWordBoundary`), so double-click + keeps intra-word punctuation like apostrophes (`can't`). New controller ops: + `selectWordAtGlobal`, `selectBlockAtGlobal`, `wordSelectionAt`, + `blockSelectionAt`, `extendSelectionGranular`, and `wordRangeIn`; new surface + geometry `MarkdownSelectionSurface.wordBoundaryForGlobal`. +- **ADDED**: Mouse cursor feedback โ€” a `MarkdownWidget` shows the click (hand) + cursor over actionable links, the text (I-beam) cursor while it participates + in a selection controller, and otherwise the default cursor. +- **CHANGED**: `MarkdownWidget`'s render object now draws the selection + highlight outside the cached content `Picture` and becomes a repaint boundary + when selectable, so selection/drag repaints do not rebuild the glyph cache. + The highlight color is now customizable via the controller / scope. The + highlight is painted on top of (rather than beneath) the glyphs, so a + translucent selection stays visible over opaque backgrounds โ€” code fences, + `inline code`, and `==marked==` spans. +- **EXAMPLE**: Reworked the demo tabs โ€” a longer, richer chat (tables, code, + nested/task lists, alerts, math, token-by-token streaming with a typing + indicator, Select-all/Clear) and a Selection tab that spans every block type. + ## 0.1.0 - **ADDED**: GitHub-style alert blocks (`> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, diff --git a/README.md b/README.md index e8d5ef6..1ede646 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,17 @@ A high-performance, lightweight Markdown parser and renderer specifically design - **๐ŸŽจ Fully Customizable**: Theme-based styling with complete control over appearance - **๐Ÿ“ฑ Flutter Native**: Built from the ground up for Flutter with custom render objects - **๐Ÿ”— Interactive Elements**: Clickable links with customizable tap handlers +- **โœ‚๏ธ Text Selection**: Cross-block and cross-widget (chat) selection via a + controller that survives list disposal and streaming updates +- **๐ŸŒŠ Streaming Parser**: `StreamingMarkdownParser` parses LLM token output + incrementally โ€” completed blocks are frozen, only the live tail re-parses - **๐ŸŒ Cross Platform**: Works on all Flutter-supported platforms - **๐Ÿ“ GitHub Flavored**: Alerts (`> [!NOTE]`), task lists (`- [x]`), tables with column alignment, thematic breaks, strikethrough, and more - **๐Ÿงฎ Inline Math**: Opt-in `$...$` LaTeX โ†’ Unicode (commands + super/subscripts) +- **๐ŸŒˆ Syntax Highlighting**: Opt-in, dependency-free code-block highlighting โ€” + 65+ languages and GitHub light/dark themes; only the languages you import are + bundled (the rest tree-shake away) - **๐ŸŽฏ AI-Optimized**: Specifically designed for AI-generated content display - **๐Ÿ”ง Extensible**: Easy to extend with custom block and span renderers - **โœ… Well Tested**: 370+ tests; parser and node model at ~100% line coverage @@ -142,16 +149,19 @@ void main() { ``` ```` +Code blocks can be syntax-highlighted โ€” see +[Syntax Highlighting](#-syntax-highlighting). + ### Tables Column alignment is supported via the delimiter row (`:---` left, `:--:` center, `---:` right): ```markdown -| Left | Center | Right | -| :------- | :------: | -------: | -| Cell 1 | Cell 2 | Cell 3 | -| **Bold** | _Italic_ | `Code` | +| Left | Center | Right | +| :------- | :------: | -----: | +| Cell 1 | Cell 2 | Cell 3 | +| **Bold** | _Italic_ | `Code` | ``` ### Links and Images @@ -169,8 +179,9 @@ Any of `---`, `***`, or `___` (optionally spaced, e.g. `- - -`) produce a rule: ```markdown --- -*** -___ +--- + +--- ``` ## ๐Ÿš€ Quick Start @@ -190,6 +201,219 @@ Then run: flutter pub get ``` +> **Upgrading from 0.0.x?** See the +> [Migration guide: 0.0.x โ†’ 0.2.x](docs/migration/0.0.x-to-0.2.x.md). The upgrade +> is almost entirely backward compatible โ€” most apps need no code changes. + +## โœ‚๏ธ Text Selection + +Selection is anchored on the immutable Markdown model, not on the render +objects, so it spans multiple blocks (heading โ†’ paragraph โ†’ list โ†’ table cell) +**and** multiple `MarkdownWidget`s (e.g. chat messages), and it survives widgets +being scrolled off-screen and disposed. Wrap a group of widgets in a +`MarkdownSelectionScope`, give each a stable `documentId`, and register the +models with the controller: + +```dart +final controller = MarkdownSelectionController(); + +// Register the documents in reading order (a chat feeds this from its list). +controller.setDocuments([ + for (final (i, m) in messages.indexed) + MarkdownDocumentRef(id: m.id, model: m.markdown, order: i), +]); + +MarkdownSelectionScope( + controller: controller, + child: ListView.builder( + itemCount: messages.length, + itemBuilder: (context, i) => MarkdownWidget( + documentId: messages[i].id, + markdown: messages[i].markdown, + ), + ), +); + +// Any time โ€” even for messages scrolled off-screen: +final String text = controller.getText(); // default formatter +final String md = controller.getText(const MarkdownMarkupFormatter()); // as Markdown +final MarkdownSelectedContent structured = controller.selectedContent(); +``` + +- **Every block is selectable.** Paragraphs, headings, quotes, code, alerts, + lists and tables โ€” a drag can start or end inside a list item or table cell, + and copied text preserves the list `\n` / table `\t` separators. +- **Get the text your way.** `getText()` uses the default + `MarkdownPlainTextFormatter` (configurable block/document separators). For + richer output pass the built-in `MarkdownMarkupFormatter` ("Copy as + Markdown"): it re-emits heading `#`s, nested list markers with task + checkboxes, blockquote/alert `>` prefixes, fenced code and pipe tables for + fully-selected blocks (partially-selected edges fall back to plain text). Or + implement your own `MarkdownSelectionFormatter`. `selectedContent()` returns + the structured per-document / per-block result each formatter consumes. +- **Streaming stays anchored.** Call `controller.putDocument(id, newModel)` when + a message grows; the default `MarkdownReconciliationPolicy.contentAnchored` + keeps the selection (append fast-path, else relocate by content, else clamp). +- **One selection at a time.** Share a `MarkdownSelectionGroup` between + controllers so selecting in one clears the others; call `group.clearExternal()` + when a plain `SelectableText`/`SelectionArea` starts its own selection. +- **Gestures.** A mouse/trackpad/stylus drag selects; on touch a + long-press-then-drag selects (so a plain swipe still scrolls the list). +- **Native handles, magnifier & toolbar.** On touch platforms the selection + shows draggable handles and a magnifier (they follow the content as it + scrolls, even across widgets); right-click (desktop) or long-press (mobile) + shows an adaptive Copy / Select-all toolbar. +- **Keyboard shortcuts.** When the scope is focused: `Ctrl/Cmd+C` copies, + `Ctrl/Cmd+A` selects all, `Shift`+arrows extend (character / word / line / + document, plus vertical), `Esc` clears โ€” using the ambient + `DefaultTextEditingShortcuts`. +- **Customizable like `SelectableText`.** `MarkdownSelectionScope` takes + `selectionColor`, `contextMenuBuilder`, `magnifierConfiguration`, + `selectionControls`, `focusNode`, `enabled` and `onSelectionChanged`; its + public `MarkdownSelectionScopeState` exposes `copySelection` / `selectAll` / + `clearSelection` / `showToolbar` / `contextMenuButtonItems` / + `contextMenuAnchors` for a fully custom menu. +- **Opt-in & compatible.** A `MarkdownWidget` with no `documentId`/controller is + inert โ€” existing usage is unchanged. + +```dart +MarkdownSelectionScope( + controller: controller, + selectionColor: Colors.amber.withValues(alpha: 0.3), + onSelectionChanged: (sel) => debugPrint('selection: $sel'), + contextMenuBuilder: (context, state) => AdaptiveTextSelectionToolbar.buttonItems( + anchors: state.contextMenuAnchors, + buttonItems: [ + ...state.contextMenuButtonItems, // Copy, Select all + ContextMenuButtonItem( + label: 'Copy LOUD', + onPressed: () => Clipboard.setData( + ClipboardData(text: state.controller.getText().toUpperCase())), + ), + ], + ), + child: /* ... */, +); +``` + +See the runnable **Selection** and **Chat** tabs in `example/`. + +## ๐ŸŒŠ Streaming (LLM output) + +LLM replies arrive token by token. Re-parsing the whole accumulated buffer on +every token is `O(Nยฒ)` and janks long messages. `StreamingMarkdownParser` keeps +the accumulated source and only re-parses the still-growing **tail**: once a +block is provably complete (terminated by a blank line, and not inside an open +code fence) it is _frozen_ and never parsed again. + +```dart +final parser = StreamingMarkdownParser(); + +llmTokenStream.listen((token) { + final Markdown md = parser.add(token); // cheap, incremental + setState(() => _message = md); +}); +``` + +Or transform a `Stream` directly โ€” each event emits the grown document: + +```dart +llmTokenStream + .toMarkdown() // Stream โ†’ Stream + .listen((md) => setState(() => _message = md)); +``` + +- **Exact, never approximate.** The result of `add()` / `current` is always + identical, block for block, to `Markdown.fromString(everythingReceivedSoFar)`. + Blocks whose type still depends on input that hasn't arrived โ€” an unterminated + code fence, a table header still missing its delimiter row, a list that may + continue โ€” stay in the live tail and are re-evaluated, so they never freeze + into the wrong shape. +- **Same options as batch.** Pass a configured decoder to match + `Markdown.fromString`: + `StreamingMarkdownParser(decoder: const MarkdownDecoder(inlineMath: true))` + (or `.toMarkdown(decoder: ...)`). +- **Fast.** Replaying a whole message token-by-token is **3โ€“14ร— faster** than + the full-reparse approach, and the gap grows with message length + (`benchmark/streaming_benchmark.dart`). +- **Reusable.** `parser.reset()` clears state for the next message; + `parser.source` is the raw text accumulated so far, `parser.current` the + parsed model without adding anything. + +Pair it with selection: feed each grown model to `controller.putDocument(id, md)` +and the active selection stays anchored as the message streams in (see the +**Chat** tab in `example/`). + +## ๐ŸŒˆ Syntax Highlighting + +Fenced code blocks can be syntax-highlighted with a **tree-shakeable, +dependency-free** highlighter โ€” 65+ languages and ready-made GitHub light/dark +themes. Highlighting is **opt-in** and off by default (code renders as plain +monospace): assign a `SyntaxHighlighter` to `MarkdownThemeData.highlighter`. + +```dart +import 'package:flutter_md/highlight.dart'; // engine + MarkdownHighlighter +import 'package:flutter_md/highlight/dart.dart'; // one import per language +import 'package:flutter_md/highlight/sql.dart'; +import 'package:flutter_md/highlight/themes.dart'; // HighlightThemes + +final theme = MarkdownThemeData.mergeTheme( + Theme.of(context), + highlighter: MarkdownHighlighter( + // You assemble the map, so only these grammars are compiled in. + languages: { + 'dart': HighlightDart.grammar, + 'sql': HighlightSql.grammar, + }, + theme: isDark ? HighlightThemes.githubDark : HighlightThemes.githubLight, + ), +); + +MarkdownWidget(markdown: doc, theme: theme); +``` + +- **Only the languages you import ship.** Each language is its own library + exposing a single grammar, referenced through lazy thunks with **no central + registry/`Map`/`enum`**. Importing `highlight/dart.dart` never references any + other language, so unused grammars are removed by tree-shaking โ€” a Dart-only + app adds ~0 beyond the engine, and all 65 languages together add ~62 KB + (gzipped) only if you deliberately bundle them all. +- **Selection- and copy-safe.** The highlighter only *partitions* the source + into colored spans; the concatenated text is byte-identical, so cross-block + selection, offsets and "Copy as Markdown" stay aligned. +- **Themes.** Use `HighlightThemes.githubDark` / `githubLight`, or implement + `CodeHighlightTheme` (a `switch` from token type โ†’ `TextStyle`) for your own + palette. Its `background` is applied to the code-block surface. +- **Demos & tooling.** `import 'package:flutter_md/highlight/all.dart';` exposes + `allHighlightLanguages`, a ready map of every grammar keyed by tag and alias + (`js`, `ts`, `sh`, โ€ฆ). It references everything, so unused languages can no + longer be tree-shaken away โ€” use it for demos, not production. +- **Robust.** Unknown languages fall back to plain text; a pattern Dart's regex + engine rejects disables just that one rule instead of breaking the language. + +The grammars are generated by `tool/highlight_codegen` (see its README to +regenerate or add languages). See the runnable **Highlight** tab in `example/`. + +> Syntax grammars are adapted from [Prism](https://prismjs.com) (MIT License). + +```dart +// Custom theme: map token types to styles. +final class MyCodeTheme implements CodeHighlightTheme { + const MyCodeTheme(); + @override + Color? get background => const Color(0xFF1E1E1E); + @override + Color? get foreground => const Color(0xFFD4D4D4); + @override + TextStyle? styleFor(String tokenType) => switch (tokenType) { + 'comment' => const TextStyle(color: Color(0xFF6A9955)), + 'keyword' => const TextStyle(color: Color(0xFF569CD6)), + 'string' || 'string-literal' => const TextStyle(color: Color(0xFFCE9178)), + _ => null, + }; +} +``` + ## ๐ŸŽจ Customization ### Theme Configuration @@ -285,18 +509,34 @@ class _MyWidgetState extends State { ## ๐Ÿ“Š Performance -- **Parsing**: single-pass, lookup-table driven parser with a plain-text fast - path and hand-rolled (regex-free) block/inline scanning. The hot path was - rewritten for a ~45% speedup, and it parses typical AI responses roughly - **10ร— faster** than the `markdown` package. -- **Rendering**: 120 FPS smooth scrolling for chat-like interfaces -- **Memory**: Minimal memory footprint with efficient span filtering - -Benchmarks live in `benchmark/`: +`flutter_md` is built for speed: a single-pass, lookup-table parser (regex-free +hot path with a plain-text fast path) and a custom render object that lays the +whole document into one cached `ui.Picture` instead of a deep tree of per-block +widgets. + +Head-to-head against `flutter_markdown` and `gpt_markdown` on the same machine +(i7-13700K) with identical styles โ€” full tables and methodology in +[`benchmark_compare/RESULTS.md`](benchmark_compare/RESULTS.md): + +- **Parsing** โ€” **~18ร— faster** than the `markdown` package that backs + `flutter_markdown` (`gpt_markdown` has no standalone parser), sustaining + ~60 MB/s on mixed documents. +- **Rendering** (string โ†’ painted pixels) โ€” **~6.6ร— faster than + `flutter_markdown` and ~15.6ร— faster than `gpt_markdown`** on a large document + (1.2โ€“1.6ร— on small chat bubbles). +- **Scrolling** โ€” 60 fps with **zero dropped frames** in profile mode, and the + lowest UI-thread frame-build cost of the three (99th-percentile frame build + **1.6 ms** vs 4.8 / 9.4 ms). ```bash -dart run benchmark/parser_benchmark.dart # multi-scenario, vs. `markdown` +# In-repo parser micro-benchmarks: +dart run benchmark/parser_benchmark.dart # multi-scenario, vs. `markdown` dart run benchmark/compare.dart --save # low-noise before/after tool + +# Head-to-head vs flutter_markdown & gpt_markdown (parser + render): +cd benchmark_compare && flutter pub get +dart run benchmark/parser_benchmark.dart +flutter test test/render_benchmark_test.dart ``` ## ๐Ÿ”ง Advanced Features diff --git a/benchmark/render_benchmark.dart b/benchmark/render_benchmark.dart new file mode 100644 index 0000000..267bd1b --- /dev/null +++ b/benchmark/render_benchmark.dart @@ -0,0 +1,238 @@ +// RENDER BENCHMARK โ€” guards layout/paint cost and the content-Picture cache. +// +// Must run under the Flutter test engine (needs real dart:ui text layout + +// PictureRecorder); plain `dart run` has no text backend. +// +// Compare against the saved baseline: +// flutter test benchmark/render_benchmark.dart +// Record a new baseline (benchmark/.render_baseline.txt): +// flutter test benchmark/render_benchmark.dart --dart-define=RENDER_BASELINE=save +// +// Timings are RELATIVE (headless engine) โ€” use the deltas for regressions, and +// confirm true frame-rate with `flutter run --profile` + DevTools timeline. +// +// Tiers: +// layout_large full layout of a large doc (performLayout cost) +// paint_miss fresh painter: layout + first paint (records new Picture) +// paint_hit same painter+size: paint again (cache hit โ†’ drawPicture) +// stream_append update(newModel) + relayout (LLM streaming rebuild) +// scroll_frame wall time per pump during a drag (widget-level) +// selection_drag highlight repaint over cached Picture (must NOT rebuild it) +// +// The selection_drag tier encodes the spike-S7 invariant: a selection drag +// repaints only the highlight overlay, reusing the cached content Picture, so a +// drag frame must be far cheaper than a fresh (cache-miss) paint. +import 'dart:io'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_md/src/render.dart' show MarkdownPainter; +import 'package:flutter_test/flutter_test.dart'; + +const bool _save = String.fromEnvironment('RENDER_BASELINE') == 'save'; +const double _kWidth = 400; + +final Map _results = {}; + +MarkdownThemeData _theme() => MarkdownThemeData( + textStyle: const TextStyle(fontSize: 14, color: Color(0xFF000000)), + textDirection: TextDirection.ltr, + textScaler: TextScaler.noScaling, + ); + +String _largeSource({int blocks = 50}) { + final b = StringBuffer(); + for (var i = 0; i < blocks; i++) { + b.writeln('## Section $i'); + b.writeln(); + b.writeln('Paragraph $i with **bold**, _italic_, `code` and ' + '[a link](https://example.com/$i) plus some filler words here.'); + b.writeln(); + b.writeln('- item one for $i'); + b.writeln('- item two for $i'); + b.writeln(); + b.writeln('| A | B |'); + b.writeln('|---|---|'); + b.writeln('| $i | ${i * 2} |'); + b.writeln(); + } + return b.toString(); +} + +/// Min-of-batches per-op microseconds (mirrors benchmark/compare.dart). +double _bench(void Function() body, + {int warmupMs = 150, int batches = 20, int minBatchMs = 8}) { + final warm = Stopwatch()..start(); + while (warm.elapsedMilliseconds < warmupMs) body(); + + var iters = 1; + while (true) { + final sw = Stopwatch()..start(); + for (var i = 0; i < iters; i++) body(); + sw.stop(); + if (sw.elapsedMicroseconds >= minBatchMs * 1000) break; + iters = iters < 2 ? 2 : iters * 2; + if (iters > 1 << 22) break; + } + + var best = double.infinity; + for (var b = 0; b < batches; b++) { + final sw = Stopwatch()..start(); + for (var i = 0; i < iters; i++) body(); + sw.stop(); + best = math.min(best, sw.elapsedMicroseconds / iters); + } + return best; +} + +void _paintOnce(MarkdownPainter p, Size size) { + final rec = ui.PictureRecorder(); + final canvas = Canvas(rec); + p.paint(canvas, size); + rec.endRecording().dispose(); +} + +void main() { + final theme = _theme(); + final large = Markdown.fromString(_largeSource()); + final largeGrown = Markdown.fromString('${_largeSource()}\n\nAppended tail.'); + + test('micro: layout / paint-miss / paint-hit / stream', () { + // layout_large + _results['layout_large'] = _bench(() { + MarkdownPainter(markdown: large, theme: theme) + ..layout(maxWidth: _kWidth) + ..dispose(); + }); + + // paint_miss (fresh painter each time -> records a new Picture) + _results['paint_miss'] = _bench(() { + final p = MarkdownPainter(markdown: large, theme: theme); + final size = p.layout(maxWidth: _kWidth); + _paintOnce(p, size); + p.dispose(); + }); + + // paint_hit (same painter + size -> reuses the cached Picture) + final hitPainter = MarkdownPainter(markdown: large, theme: theme); + final hitSize = hitPainter.layout(maxWidth: _kWidth); + _paintOnce(hitPainter, hitSize); // prime the cache + _results['paint_hit'] = _bench(() => _paintOnce(hitPainter, hitSize)); + hitPainter.dispose(); + + // stream_append (toggle model so update() always sees a change) + final p = MarkdownPainter(markdown: large, theme: theme) + ..layout(maxWidth: _kWidth); + var flip = false; + _results['stream_append'] = _bench(() { + flip = !flip; + p + ..update(markdown: flip ? largeGrown : large, theme: theme) + ..layout(maxWidth: _kWidth); + }); + p.dispose(); + + // selection_drag (grow a highlight over the cached content Picture each + // frame; the content Picture must be reused, not re-recorded). + final selPaint = Paint()..color = const Color(0x552196F3); + final selPainter = MarkdownPainter(markdown: large, theme: theme); + final selSize = selPainter.layout(maxWidth: _kWidth); + _paintOnce(selPainter, selSize); // prime the content cache + var off = 0; + _results['selection_drag'] = _bench(() { + off = (off + 1) % 8; + final rec = ui.PictureRecorder(); + final canvas = Canvas(rec); + selPainter.paintHighlight( + canvas, + (source) => source == 0 ? TextRange(start: 0, end: off) : null, + selPaint, + ); + selPainter.paint(canvas, selSize); // cache hit โ€” no new Picture + rec.endRecording().dispose(); + }); + selPainter.dispose(); + + // Sanity: the cache must make a hit dramatically cheaper than a miss. + expect(_results['paint_hit']!, lessThan(_results['paint_miss']!)); + // A selection-drag frame must not rebuild the content Picture, so it stays + // far below a fresh paint (spike S7 invariant). + expect(_results['selection_drag']!, lessThan(_results['paint_miss']! / 3)); + }); + + testWidgets('widget: scroll_frame wall time', (tester) async { + final docs = [ + for (var i = 0; i < 30; i++) + Markdown.fromString('### Message $i\n\nBody of message $i with text.'), + ]; + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: ListView.builder( + itemCount: docs.length, + itemBuilder: (_, i) => Padding( + padding: const EdgeInsets.all(8), + child: MarkdownWidget(markdown: docs[i], theme: theme), + ), + ), + ), + )); + await tester.pumpAndSettle(); + + const frames = 40; + final sw = Stopwatch()..start(); + for (var i = 0; i < frames; i++) { + await tester.drag(find.byType(ListView), const Offset(0, -12)); + await tester.pump(const Duration(milliseconds: 16)); + } + sw.stop(); + _results['scroll_frame'] = sw.elapsedMicroseconds / frames; + }); + + tearDownAll(() { + final baseline = _loadBaseline(); + // ignore: avoid_print + print('\n${'tier'.padRight(16)}${'us/op'.padLeft(12)}' + '${'base'.padLeft(12)}${'delta'.padLeft(10)}'); + for (final e in _results.entries) { + final base = baseline?[e.key]; + final delta = base == null ? '-' : _delta(base, e.value); + // ignore: avoid_print + print('${e.key.padRight(16)}${e.value.toStringAsFixed(2).padLeft(12)}' + '${(base?.toStringAsFixed(2) ?? '-').padLeft(12)}${delta.padLeft(10)}'); + } + if (_save) { + _saveBaseline(_results); + // ignore: avoid_print + print('\nSaved baseline to benchmark/.render_baseline.txt'); + } + }); +} + +String _delta(double base, double now) { + final pct = (now - base) / base * 100; + return '${pct <= 0 ? '' : '+'}${pct.toStringAsFixed(1)}%'; +} + +Map? _loadBaseline() { + final file = File('benchmark/.render_baseline.txt'); + if (!file.existsSync()) return null; + final map = {}; + for (final line in file.readAsLinesSync()) { + final parts = line.split('\t'); + if (parts.length == 2) { + final v = double.tryParse(parts[1]); + if (v != null) map[parts[0]] = v; + } + } + return map; +} + +void _saveBaseline(Map results) { + final buffer = StringBuffer(); + for (final e in results.entries) { + buffer.writeln('${e.key}\t${e.value.toStringAsFixed(3)}'); + } + File('benchmark/.render_baseline.txt').writeAsStringSync(buffer.toString()); +} diff --git a/benchmark/streaming_benchmark.dart b/benchmark/streaming_benchmark.dart new file mode 100644 index 0000000..f2a3625 --- /dev/null +++ b/benchmark/streaming_benchmark.dart @@ -0,0 +1,138 @@ +// ignore_for_file: avoid_print + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:flutter_md/src/markdown.dart' show Markdown; +import 'package:flutter_md/src/parser.dart' show StreamingMarkdownParser; + +import 'scenarios.dart'; + +/// Streaming benchmark: replays a document token-by-token and compares the two +/// ways of keeping a parsed [Markdown] up to date on every token. +/// +/// - **full** โ€” what a naive chat UI does today: re-parse the *entire* +/// accumulated buffer on every token (`Markdown.fromString(buffer)`), which +/// is `O(Nยฒ)` over a stream. +/// - **stream** โ€” feed each token to a [StreamingMarkdownParser], which freezes +/// completed blocks and only re-parses the live tail. +/// +/// Each `measure()` replays the whole stream once, so the reported ยตs is the +/// cost of rendering one full message start-to-finish. The batch decoder is +/// byte-identical to `master` (this file adds no code to the hot path), so the +/// separate `parser_benchmark.dart` numbers are the regression guard; this file +/// shows the incremental win. +/// +/// Run: +/// ```shell +/// dart run benchmark/streaming_benchmark.dart +/// # or, for stable numbers: +/// dart compile exe benchmark/streaming_benchmark.dart -o /tmp/sb && /tmp/sb +/// ``` +void main() { + final docs = { + 'prose': scenarios['prose']!, + 'lists': scenarios['lists']!, + 'table': scenarios['table']!, + 'code': scenarios['code']!, + 'quotes': scenarios['quotes']!, + 'mixed': scenarios['mixed']!, + 'big-chat': _bigChat(80), + }; + + print('Replaying each document token-by-token (one full stream per op).'); + print(''); + print('scenario tokens chars full ms stream ms speedup'); + print('--------- ------ ------ -------- ---------- --------'); + for (final entry in docs.entries) { + final doc = entry.value; + final tokens = _tokenize(doc); + final full = _FullReparse(tokens).measure(); // us per full replay + final stream = _Streaming(tokens).measure(); // us per full replay + final speedup = full / stream; + print('${entry.key.padRight(9)} ' + '${tokens.length.toString().padLeft(6)} ' + '${doc.length.toString().padLeft(6)} ' + '${(full / 1000).toStringAsFixed(2).padLeft(8)} ' + '${(stream / 1000).toStringAsFixed(2).padLeft(10)} ' + '${'${speedup.toStringAsFixed(2)}x'.padLeft(8)}'); + } + print(''); + print('Equivalence is proven in test/parser/streaming_test.dart โ€” the stream ' + 'result matches Markdown.fromString at every prefix.'); +} + +/// Splits [doc] into word-ish tokens that reconstruct it exactly, approximating +/// how an LLM streams sub-word/word tokens (whitespace rides along). +List _tokenize(String doc) { + final parts = doc.split(' '); + return [ + for (var i = 0; i < parts.length; i++) i == 0 ? parts[i] : ' ${parts[i]}', + ]; +} + +/// Re-parses the whole accumulated buffer on every token (the `O(Nยฒ)` path). +class _FullReparse extends BenchmarkBase { + _FullReparse(this.tokens) : super('full'); + + final List tokens; + Markdown? _result; + + @override + void run() { + final buffer = StringBuffer(); + for (final token in tokens) { + buffer.write(token); + _result = Markdown.fromString(buffer.toString()); + } + } + + @override + void teardown() { + super.teardown(); + if (_result == null) throw StateError('result is null'); + } +} + +/// Feeds each token to a [StreamingMarkdownParser] (the incremental path). +class _Streaming extends BenchmarkBase { + _Streaming(this.tokens) : super('stream'); + + final List tokens; + Markdown? _result; + + @override + void run() { + final parser = StreamingMarkdownParser(); + for (final token in tokens) { + _result = parser.add(token); + } + } + + @override + void teardown() { + super.teardown(); + if (_result == null) throw StateError('result is null'); + } +} + +/// Builds a long, blank-separated chat message with [blocks] varied blocks, so +/// the incremental parser has plenty of completed blocks to freeze. +String _bigChat(int blocks) { + final buffer = StringBuffer(); + for (var i = 0; i < blocks; i++) { + switch (i % 5) { + case 0: + buffer.write('## Section $i\n\n'); + case 1: + buffer.write('A paragraph with **bold**, _italic_ and `code` number ' + '$i to give the inline parser some real work to do.\n\n'); + case 2: + buffer.write('- item ${i}a\n- item ${i}b\n- item ${i}c\n\n'); + case 3: + buffer.write('| Col | Val |\n| --- | --: |\n' + '| a | $i |\n| b | ${i + 1} |\n\n'); + case 4: + buffer.write('```dart\nfinal x$i = $i;\nprint(x$i);\n```\n\n'); + } + } + return buffer.toString(); +} diff --git a/benchmark_compare/.gitignore b/benchmark_compare/.gitignore new file mode 100644 index 0000000..615e140 --- /dev/null +++ b/benchmark_compare/.gitignore @@ -0,0 +1,12 @@ +.dart_tool/ +build/ +pubspec.lock +*.exe + +# Generated platform scaffolding (regenerate with: +# flutter create --platforms=linux --project-name md_benchmark_compare .) +linux/ +.metadata +.flutter-plugins-dependencies +*.iml +.idea/ diff --git a/benchmark_compare/README.md b/benchmark_compare/README.md new file mode 100644 index 0000000..da58e41 --- /dev/null +++ b/benchmark_compare/README.md @@ -0,0 +1,126 @@ +# Markdown benchmark comparison + +A standalone, non-published package that pits **flutter_md** against two popular +alternatives โ€” [`flutter_markdown`](https://pub.dev/packages/flutter_markdown) +and [`gpt_markdown`](https://pub.dev/packages/gpt_markdown) โ€” on both **parsing** +and **rendering**. + +It lives inside the repo (path dependency on `../`) but is excluded from +`flutter_md` publishing via the root `.pubignore`. + +## Results at a glance + +Measured on an i7-13700K (Flutter 3.41.6), all three libraries given identical +normalized styles. Full tables in **[`RESULTS.md`](RESULTS.md)**. + +- **Parser:** flutter_md is **~18ร— faster** than the `markdown` package that + backs flutter_markdown (gpt_markdown has no separable parser). +- **Render (string โ†’ painted pixels):** flutter_md is **1.4โ€“15.6ร— faster** + end-to-end โ€” ~6.6ร— vs flutter_markdown and ~15.6ร— vs gpt_markdown on the large + document. +- **Profile-mode scroll:** all three hold 60 fps with **0 dropped frames** on + desktop; flutter_md has the lowest UI-thread build cost (99th-percentile frame + build **1.6 ms** vs 4.8 / 9.4 ms) and, at equal on-screen content, the lowest + raster time too. + +## Layout + +| File | What it measures | How to run | +| ---- | ---------------- | ---------- | +| `lib/corpus.dart` | Shared benchmark documents (simple โ†’ complex/large), common-denominator GFM only. | โ€” | +| `lib/styles.dart` | Normalized styles shared by all three libraries: one base text style + explicit line height, matched heading sizes and block spacing. | โ€” | +| `benchmark/parser_benchmark.dart` | **Parser** cost via `benchmark_harness`: `flutter_md` vs the `markdown` package (which `flutter_markdown` uses internally). | `dart run benchmark/parser_benchmark.dart` | +| `test/render_benchmark_test.dart` | **Render** cost via widget tests: end-to-end string โ†’ painted pixels for all three libraries (headless). | `flutter test test/render_benchmark_test.dart` | +| `integration_test/scroll_perf_test.dart` + `test_driver/perf_driver.dart` | **Profile-mode** confirmation: real frame build/raster times while scrolling a feed, captured as a `TimelineSummary`. | see below | +| `tool/summarize_timeline.dart` | Formats the profile-mode JSON into a table. | `dart run tool/summarize_timeline.dart` | + +## Running + +```shell +cd benchmark_compare +flutter pub get + +# Parser โ€” JIT is fine, AOT is the most stable: +dart run benchmark/parser_benchmark.dart +dart compile exe benchmark/parser_benchmark.dart -o /tmp/pb && /tmp/pb + +# Render โ€” must run under the Flutter test engine (needs real text layout): +flutter test test/render_benchmark_test.dart + +# Profile-mode confirmation โ€” real frame timings on a device (here: Linux +# desktop). Needs the platform runner: regenerate it once with +# flutter create --platforms=linux --project-name md_benchmark_compare . +flutter drive --driver=test_driver/perf_driver.dart \ + --target=integration_test/scroll_perf_test.dart --profile -d linux +dart run tool/summarize_timeline.dart # -> reads build/integration_response_data.json + +# Controlled variants for an apples-to-apples raster comparison: +# FEED=prose one prose doc repeated -> identical content per frame +# ITEM_MODE=fixed every item clipped to a constant height +flutter drive --driver=test_driver/perf_driver.dart \ + --target=integration_test/scroll_perf_test.dart --profile -d linux \ + --dart-define=FEED=prose +``` + +The parser and render entry points print ready-to-paste Markdown tables; the +profile-mode run writes `build/integration_response_data.json`, which +`tool/summarize_timeline.dart` turns into a table. A captured run of all three is +in [`RESULTS.md`](RESULTS.md). + +> The generated `linux/` runner (and `.metadata`, `*.iml`, โ€ฆ) are gitignored as +> throwaway scaffolding โ€” regenerate with the `flutter create` line above. Swap +> `-d linux` for another device (`-d chrome`, a mobile device/emulator) if you +> prefer; the harness is platform-agnostic. + +## Methodology & fairness + +**Corpus.** Documents stick to common-denominator GFM (headings, emphasis, +inline code, links, blockquotes, fenced code, ordered/unordered/task lists, +tables) so all three libraries render them without diverging on dialect-specific +extensions. Images are omitted so render timings measure text layout, not +network image stubs. + +**Normalized styles.** All three libraries are configured from `lib/styles.dart` +with the same base text style (font size 14, explicit line `height` 1.4, same +color), the same heading sizes (base + {10,8,6,4,2,0}, bold) and matched block +spacing. An explicit line height makes every line box `fontSize ร— height`, +independent of font metrics, so text lays out at the same vertical rhythm for +every library and in both the headless-test and profile-desktop environments. +This removes layout *density* as a confound in the render/raster comparison โ€” a +more compact renderer would otherwise rasterize more content per frame. Purely +structural chrome (code-block frames, table borders, list bullets/indents, +gpt_markdown's code header) is not unifiable through public style APIs and +remains; the render benchmark prints a height table that quantifies the residual. + +**Parser.** `flutter_markdown` does no parsing of its own โ€” on every build it +delegates to the `markdown` package (`md.Document(...).parse(source)`). So the +apples-to-apples parser comparison is `flutter_md`'s `Markdown.fromString` +against that same `markdown` package (a fresh single-use `Document` per parse, +exactly as `flutter_markdown` uses it). `gpt_markdown` has **no separable +parser** โ€” it parses inline while building its widget tree โ€” so it does not +appear in the parser table; its parse cost is folded into the render numbers +instead. Each `benchmark_harness` benchmark overrides `exercise()` to run once, +so the reported figure is microseconds **per single parse**. + +**Render.** Measures the *end-to-end* cost of turning a Markdown **string** into +painted pixels โ€” parse + build + layout + paint โ€” which is what every library +does on the frame that first shows a message. A changing `ValueKey` forces a +full subtree teardown + rebuild each iteration, and wall-clock time is taken +around `tester.pump()` (min-of-batches, the most stable estimator). To keep it +fair: + +- Every iteration reconstructs from the source string. For `flutter_md` that + means `Markdown.fromString(src)` runs inside the builder too, so its parse + cost is included on equal footing. +- All three render into the same fixed-width (400px) viewport inside a + `SingleChildScrollView`, so layout covers the whole document while paint is + clipped to the same visible region for every library. + +**Interpreting the numbers.** Render microseconds (headless widget test) include +a fixed `flutter_test` per-frame overhead (~1 ms floor), so they are meaningful +only *relative to each other on the same machine* โ€” use the ratios, and note +that the floor compresses the small-document ratios while the large/complex +documents show the true gap. The **profile-mode scroll benchmark** removes that +floor and confirms the ordering on a real device with true GPU rasterization +(same data you'd read off the DevTools Performance timeline, but automated). +Parser microseconds have no such floor and are directly comparable. diff --git a/benchmark_compare/RESULTS.md b/benchmark_compare/RESULTS.md new file mode 100644 index 0000000..f672237 --- /dev/null +++ b/benchmark_compare/RESULTS.md @@ -0,0 +1,131 @@ +# Benchmark results + +Head-to-head numbers for **flutter_md** vs **flutter_markdown** vs +**gpt_markdown**. Machine-specific โ€” regenerate with the commands in +[`README.md`](README.md). All three libraries use the same normalized styles +([`lib/styles.dart`](lib/styles.dart)). **Lower is better** throughout. + +## Environment + +| | | +| ----------------- | ------------------------------------------ | +| CPU | 13th Gen Intel Core i7-13700K (24 threads) | +| OS | Linux 7.1.3 (CachyOS) | +| Flutter | 3.41.6 (stable) | +| Dart | 3.11.4 | +| flutter_md | 0.2.0 (path `../`) | +| flutter_markdown | 0.7.7+1 | +| gpt_markdown | 1.1.8 | +| markdown (engine) | 7.3.1 | + +## Parser (ยตs per parse) + +Parse only, AOT-compiled: `flutter_md.Markdown.fromString` vs the `markdown` +package (flutter_markdown's engine). gpt_markdown has no separable parser. +`speedup = markdown-pkg / flutter_md`. + +| scenario | bytes | flutter_md | flutter_md MB/s | markdown-pkg | speedup | +| ------------- | ----: | ---------: | --------------: | -----------: | ---------: | +| simple | 340 | 3.08 | 110.2 | 87.01 | 28.21x | +| inline | 388 | 4.52 | 85.9 | 100.71 | 22.29x | +| lists | 344 | 5.72 | 60.1 | 100.09 | 17.49x | +| table | 471 | 7.94 | 59.4 | 105.08 | 13.24x | +| code | 290 | 1.39 | 208.0 | 23.99 | 17.21x | +| quotes | 315 | 3.22 | 97.8 | 71.15 | 22.08x | +| complex | 1030 | 16.76 | 61.4 | 275.78 | 16.45x | +| complex_large | 8256 | 130.53 | 63.3 | 2318.79 | 17.76x | +| **TOTAL** | | **173.17** | | **3082.62** | **17.80x** | + +## Render โ€” end-to-end (ยตs per string โ†’ painted pixels) + +Parse + build + layout + paint, headless widget test at 400 px width. Absolute +ยตs include a fixed `flutter_test` per-frame overhead โ€” compare via the ratios. + +| document | flutter_md | flutter_markdown | gpt_markdown | +| ------------- | ---------: | ---------------: | -----------: | +| simple | 1147.3 | 1717.3 | 1335.3 | +| inline | 1142.0 | 1619.3 | 1475.8 | +| lists | 1321.5 | 3991.3 | 7077.5 | +| table | 1521.5 | 3804.5 | 4452.0 | +| code | 818.5 | 2498.8 | 4014.8 | +| quotes | 818.8 | 1292.5 | 1316.3 | +| complex | 1447.3 | 5087.0 | 11339.8 | +| complex_large | 2678.0 | 17696.8 | 41847.5 | + +Relative to flutter_md (ร— = library / flutter_md): + +| document | flutter_md | flutter_markdown | gpt_markdown | +| ------------- | ---------: | ---------------: | -----------: | +| simple | 1.00x | 1.50x | 1.16x | +| inline | 1.00x | 1.42x | 1.29x | +| lists | 1.00x | 3.02x | 5.36x | +| table | 1.00x | 2.50x | 2.93x | +| code | 1.00x | 3.05x | 4.91x | +| quotes | 1.00x | 1.58x | 1.61x | +| complex | 1.00x | 3.51x | 7.84x | +| complex_large | 1.00x | 6.61x | 15.63x | + +## Rendered height (px) + +At 376 px width. Confirms the normalized styles give the same vertical rhythm; +the remaining spread is structural chrome (list indent, table borders, +gpt_markdown's code-block header). + +| document | flutter_md | flutter_markdown | gpt_markdown | +| ------------- | ---------: | ---------------: | -----------: | +| simple | 294 | 254 | 296 | +| inline | 294 | 294 | 296 | +| quotes | 274 | 280 | 294 | +| lists | 348 | 582 | 424 | +| table | 382 | 582 | 250 | +| code | 380 | 340 | 514 | +| complex | 1352 | 1548 | 1525 | +| complex_large | 5520 | 6234 | 6148 | + +## Profile mode โ€” scroll frame timings (ms) + +Profile build on the Linux desktop device, flinging the feed up and down; +`TimelineSummary`, mean of 2 runs. 60 Hz frame budget = 16.7 ms. + +**Mixed feed** (60 messages cycling through the corpus): + +| metric | flutter_md | flutter_markdown | gpt_markdown | +| -------------------- | ---------: | ---------------: | -----------: | +| frame build avg | 0.31 | 0.49 | 0.67 | +| frame build 90th pct | 0.70 | 0.70 | 0.94 | +| frame build 99th pct | 1.59 | 4.78 | 9.38 | +| frame build worst | 3.80 | 6.41 | 13.11 | +| raster avg | 1.10 | 0.89 | 0.95 | +| raster 90th pct | 1.85 | 1.54 | 1.53 | +| raster 99th pct | 2.79 | 2.18 | 2.57 | +| missed frames | 0 | 0 | 0 | + +**Prose feed** (one prose document repeated โ†’ identical content per frame in all +three, so no density or chrome differences): + +| metric | flutter_md | flutter_markdown | gpt_markdown | +| -------------------- | ---------: | ---------------: | -----------: | +| frame build avg | 0.36 | 1.00 | 0.62 | +| frame build 90th pct | 0.89 | 2.51 | 2.16 | +| frame build 99th pct | 1.98 | 6.03 | 4.08 | +| frame build worst | 2.84 | 7.56 | 6.61 | +| raster avg | 1.45 | 1.98 | 1.75 | +| raster 90th pct | 2.35 | 2.97 | 2.77 | +| raster 99th pct | 3.62 | 4.61 | 4.33 | +| missed frames | 0 | 0 | 0 | + +## Regenerate + +```shell +dart compile exe benchmark/parser_benchmark.dart -o /tmp/pb && /tmp/pb +flutter test test/render_benchmark_test.dart + +flutter drive --driver=test_driver/perf_driver.dart \ + --target=integration_test/scroll_perf_test.dart --profile -d linux +dart run tool/summarize_timeline.dart + +# identical content per frame: +flutter drive --driver=test_driver/perf_driver.dart \ + --target=integration_test/scroll_perf_test.dart --profile -d linux \ + --dart-define=FEED=prose +``` diff --git a/benchmark_compare/analysis_options.yaml b/benchmark_compare/analysis_options.yaml new file mode 100644 index 0000000..e56f5a1 --- /dev/null +++ b/benchmark_compare/analysis_options.yaml @@ -0,0 +1,8 @@ +# Inherit the repo's analysis rules, but relax two that are noise for a +# throwaway benchmark package full of public constants and wide table strings. +include: ../analysis_options.yaml + +linter: + rules: + public_member_api_docs: false + lines_longer_than_80_chars: false diff --git a/benchmark_compare/benchmark/parser_benchmark.dart b/benchmark_compare/benchmark/parser_benchmark.dart new file mode 100644 index 0000000..7c6de6e --- /dev/null +++ b/benchmark_compare/benchmark/parser_benchmark.dart @@ -0,0 +1,118 @@ +// ignore_for_file: avoid_print +// +// PARSER BENCHMARK โ€” flutter_md vs the `markdown` package (flutter_markdown's +// engine), using benchmark_harness. +// +// flutter_markdown does no parsing of its own; on every build it delegates to +// the `markdown` package (`md.Document(...).parse(source)`). So the fair +// parser-to-parser comparison is: +// +// flutter_md : Markdown.fromString(source) -> block model +// flutter_markdown engine : md.Document(gfm).parse(source) -> AST nodes +// +// gpt_markdown has **no separable parser** โ€” it parses inline while building +// its widget tree โ€” so it does not appear here; its parsing cost is folded into +// the render benchmark (test/render_benchmark_test.dart) instead. +// +// Run (JIT): dart run benchmark/parser_benchmark.dart +// Run (AOT, best) dart compile exe benchmark/parser_benchmark.dart -o /tmp/pb && /tmp/pb +// +// Each benchmark overrides `exercise()` to call `run()` once, so the number +// reported by `measure()` is microseconds per single parse (per-op), not the +// benchmark_harness default of 10 runs per exercise. + +import 'package:benchmark_harness/benchmark_harness.dart'; +// Import the pure-Dart parser entry point directly (not the `flutter_md.dart` +// barrel, which pulls in Flutter widgets) so this runs under `dart`/AOT with no +// Flutter engine. +import 'package:flutter_md/src/markdown.dart' show Markdown; +import 'package:markdown/markdown.dart' as md; +import 'package:md_benchmark_compare/corpus.dart'; + +/// DCE guard โ€” accumulates a byte of each result so the AOT compiler cannot +/// eliminate the parse as dead code. +int _sink = 0; + +void main() { + final rows = <_Row>[]; + for (final entry in corpus.entries) { + final bytes = entry.value.length; + final fmd = _FlutterMdBenchmark(entry.value).measure(); + final pkg = _MarkdownPkgBenchmark(entry.value).measure(); + rows.add(_Row(entry.key, bytes, fmd, pkg)); + } + + if (_sink == 0x7fffffff) print('(unreachable sink marker)'); + + _printTable(rows); +} + +void _printTable(List<_Row> rows) { + print(''); + print('Parser: flutter_md vs `markdown` pkg (flutter_markdown engine)'); + print('Lower us/op is better; speedup = markdown-pkg / flutter_md.'); + print(''); + print('| scenario | bytes | flutter_md us/op | flutter_md MB/s ' + '| markdown-pkg us/op | speedup |'); + print('| -------------- | ------: | ---------------: | --------------: ' + '| -----------------: | ------: |'); + + var fmdTotal = 0.0; + var pkgTotal = 0.0; + for (final r in rows) { + fmdTotal += r.fmd; + pkgTotal += r.pkg; + final mbps = r.bytes / r.fmd; // bytes/us == MB/s + final speedup = r.pkg / r.fmd; + print('| ${r.name.padRight(14)} ' + '| ${r.bytes.toString().padLeft(7)} ' + '| ${r.fmd.toStringAsFixed(2).padLeft(16)} ' + '| ${mbps.toStringAsFixed(1).padLeft(15)} ' + '| ${r.pkg.toStringAsFixed(2).padLeft(18)} ' + '| ${'${speedup.toStringAsFixed(2)}x'.padLeft(7)} |'); + } + final totalSpeedup = pkgTotal / fmdTotal; + print('| ${'TOTAL'.padRight(14)} ' + '| ${''.padLeft(7)} ' + '| ${fmdTotal.toStringAsFixed(2).padLeft(16)} ' + '| ${''.padLeft(15)} ' + '| ${pkgTotal.toStringAsFixed(2).padLeft(18)} ' + '| ${'${totalSpeedup.toStringAsFixed(2)}x'.padLeft(7)} |'); + print(''); +} + +class _Row { + _Row(this.name, this.bytes, this.fmd, this.pkg); + final String name; + final int bytes; + final double fmd; + final double pkg; +} + +class _FlutterMdBenchmark extends BenchmarkBase { + _FlutterMdBenchmark(this.input) : super('flutter_md'); + final String input; + + @override + void exercise() => run(); // one parse per exercise -> measure() is per-op. + + @override + void run() => _sink ^= Markdown.fromString(input).blocks.length; +} + +class _MarkdownPkgBenchmark extends BenchmarkBase { + _MarkdownPkgBenchmark(this.input) : super('markdown'); + final String input; + + @override + void exercise() => run(); + + @override + void run() { + // A fresh Document per parse โ€” exactly how flutter_markdown uses the + // package on every rebuild (Document is single-use / stateful). + final nodes = md.Document(extensionSet: md.ExtensionSet.gitHubFlavored) + .parse(input); + _sink ^= nodes.length; + } +} diff --git a/benchmark_compare/devtools_options.yaml b/benchmark_compare/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/benchmark_compare/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/benchmark_compare/integration_test/scroll_perf_test.dart b/benchmark_compare/integration_test/scroll_perf_test.dart new file mode 100644 index 0000000..eabb9ac --- /dev/null +++ b/benchmark_compare/integration_test/scroll_perf_test.dart @@ -0,0 +1,126 @@ +// PROFILE-MODE SCROLL BENCHMARK +// flutter_md vs flutter_markdown vs gpt_markdown. +// +// The widget-test render benchmark (test/render_benchmark_test.dart) measures +// wall-clock around `tester.pump()` in the *headless* flutter_tester engine, +// which carries a fixed ~1 ms per-frame floor and does no real GPU raster. +// This test confirms those results on a **real device in profile mode**: it +// scrolls a feed of Markdown "messages" per library, capturing a +// TimelineSummary +// (the same data DevTools' Performance timeline shows) โ€” real frame *build* and +// *rasterizer* times, with 90th/99th percentiles and missed-frame counts. +// +// Run: +// flutter drive \ +// --driver=test_driver/perf_driver.dart \ +// --target=integration_test/scroll_perf_test.dart \ +// --profile -d linux +// +// The per-library summaries are written to build/integration_response_data.json +// by the driver; tool/summarize_timeline.dart formats them into a table. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:md_benchmark_compare/corpus.dart'; +import 'package:md_benchmark_compare/styles.dart'; + +// All three libraries use the shared normalized styles from lib/styles.dart, so +// the feed renders at the same layout density for every library. +final Map _libraries = styledLibraries; + +// Feed mode: 'mixed' (representative chat feed) or 'prose' (one prose doc +// repeated). The 'prose' feed is a control: with normalized styles the `inline` +// doc renders to near-identical height in all three libraries (294/294/296 px) +// and has no structural chrome, so any raster difference on it is due purely to +// the rendering model (single cached ui.Picture vs a widget tree), not layout +// density or code/table/list chrome. +const String _feedMode = + String.fromEnvironment('FEED', defaultValue: 'mixed'); + +/// A realistic chat feed: representative documents, cycled. Excludes the huge +/// `complex_large` tier so many messages fit in a scrollable list. +final List _feed = _feedMode == 'prose' + ? [corpus['inline']!] + : [ + corpus['simple']!, + corpus['inline']!, + corpus['quotes']!, + corpus['lists']!, + corpus['code']!, + corpus['table']!, + corpus['complex']!, + ]; + +const int _feedLength = 60; + +const Key _feedKey = ValueKey('feed'); + +// Item sizing mode: 'natural' (each item its own height) or 'fixed' (every item +// clipped to a constant height, so a fling of the same distance crosses the +// same number of items for every library โ€” this controls for the fact that a +// more compact renderer packs more items per screen and therefore rasterizes +// more of them per unit scroll). +const String _itemMode = + String.fromEnvironment('ITEM_MODE', defaultValue: 'natural'); +const double _kFixedItemHeight = 320; + +Widget _sizeItem(Widget child) { + if (_itemMode != 'fixed') return child; + return SizedBox( + height: _kFixedItemHeight, + child: ClipRect( + child: OverflowBox( + alignment: Alignment.topLeft, + minHeight: 0, + maxHeight: double.infinity, + child: child, + ), + ), + ); +} + +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + for (final entry in _libraries.entries) { + final lib = entry.key; + final factory = entry.value; + + testWidgets('scroll perf: $lib', (tester) async { + await tester.pumpWidget( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: ListView.builder( + key: _feedKey, + itemCount: _feedLength, + itemBuilder: (context, i) => Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: _sizeItem(factory(context, _feed[i % _feed.length])), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final listFinder = find.byKey(_feedKey); + + // Trace a series of flings (down then back up) โ€” this builds, lays out + // and rasterizes many items, exercising the real frame pipeline. + await binding.watchPerformance( + () async { + for (var i = 0; i < 6; i++) { + await tester.fling(listFinder, const Offset(0, -600), 2000); + await tester.pumpAndSettle(); + await tester.fling(listFinder, const Offset(0, 600), 2000); + await tester.pumpAndSettle(); + } + }, + reportKey: 'scroll_$lib', + ); + }); + } +} diff --git a/benchmark_compare/lib/corpus.dart b/benchmark_compare/lib/corpus.dart new file mode 100644 index 0000000..6bf494f --- /dev/null +++ b/benchmark_compare/lib/corpus.dart @@ -0,0 +1,197 @@ +/// Shared benchmark corpus used by both the parser benchmark +/// (`benchmark/parser_benchmark.dart`) and the render benchmark +/// (`test/render_benchmark_test.dart`). +/// +/// The documents deliberately stick to **common-denominator GFM** (headings, +/// emphasis, inline code, links, blockquotes, fenced code, ordered/unordered/ +/// task lists, tables) so that all three libraries โ€” flutter_md, +/// flutter_markdown and gpt_markdown โ€” render them without diverging on +/// dialect-specific extensions. Images are intentionally omitted so the render +/// numbers measure text layout, not network image stubs. +library; + +/// The benchmark documents, keyed by a short name and ordered from the smallest +/// / simplest to the largest / most complex. +final Map corpus = { + 'simple': _simple, + 'inline': _inline, + 'lists': _lists, + 'table': _table, + 'code': _code, + 'quotes': _quotes, + 'complex': _complex, + // A large document: the kitchen-sink `complex` doc repeated so per-op timings + // are stable and the O(n) cost of each library is visible. + 'complex_large': _repeat(_complex, 8), +}; + +/// The subset used for the render benchmark. Rendering a very large document +/// through three widget trees is slow, so we cap the large tier at a smaller +/// repeat count than the parser benchmark uses. +final Map renderCorpus = { + 'simple': _simple, + 'inline': _inline, + 'lists': _lists, + 'table': _table, + 'code': _code, + 'quotes': _quotes, + 'complex': _complex, + 'complex_large': _repeat(_complex, 4), +}; + +String _repeat(String block, int times) { + final buffer = StringBuffer(); + for (var i = 0; i < times; i++) { + buffer + ..write(block) + ..write('\n\n'); + } + return buffer.toString(); +} + +/// Simple rich text: a couple of paragraphs with inline emphasis, code and a +/// link โ€” the "chat bubble" case. +const String _simple = ''' +Hello **world**, this is a *simple* rich-text paragraph with some `inline code` +and [a link](https://example.com) mixed into ordinary prose to exercise the +common inline formatting path. + +A second paragraph follows with a bit more **bold**, some _italic_ and a final +~~struck-through~~ span so the inline parser has a little of everything. +'''; + +/// Emphasis-heavy inline stress: many markers on every line. +const String _inline = ''' +This line has **bold**, *italic*, ***bold italic***, `code`, ~~strike~~ and a +[link](https://example.com) all packed together to stress the inline scanner. + +Nested **bold with *italic* and `code` inside** it, then *italic with **bold** +inside* and a trailing `code span` โ€” repeated markers keep the closer lookahead +busy across the whole paragraph without producing much block structure. +'''; + +/// Nested and task lists. +const String _lists = ''' +- First item with **bold** +- Second item with [a link](https://example.com) + - Nested item one + - Nested item two with `code` + - Deep item with *italic* +- Back to the top level + +1. Ordered one +2. Ordered two + 1. Sub one + 2. Sub two +3. Ordered three + +- [x] Completed task +- [ ] Pending task +- [ ] Another pending task with **emphasis** +'''; + +/// GFM tables with alignment and inline formatting inside cells. +const String _table = ''' +| Name | Age | Role | Notes | +| :------ | --: | :--------: | ---------------- | +| Alice | 25 | Developer | Likes **bold** | +| Bob | 30 | Designer | Uses `tools` | +| Charlie | 35 | Manager | ~~former~~ lead | + +| Metric | Q1 | Q2 | Q3 | Q4 | +| ----------- | ---- | ---- | ---- | ---- | +| Revenue | 100 | 120 | 140 | 180 | +| Growth | 10% | 20% | 17% | 29% | +| Active users| 1.2k | 1.5k | 1.9k | 2.4k | +'''; + +/// Fenced code blocks in a couple of languages. +const String _code = ''' +```dart +void main() { + final greeting = 'Hello, world!'; + for (var i = 0; i < 10; i++) { + print(greeting); + } +} +``` + +Some prose between two code blocks to break them apart cleanly. + +```python +def fib(n): + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a +``` +'''; + +/// Blockquotes, including nested formatting. +const String _quotes = ''' +> This is a blockquote that spans several lines and contains **bold** text, +> some `inline code` and [a link](https://example.com) inside the body. + +> A second quote paragraph. +> +> It has multiple paragraphs and even a bit of *emphasis* to keep the inline +> parser working while inside a block-level quote context. +'''; + +/// The kitchen-sink document: every common block type, in one place. Used both +/// on its own (`complex`) and repeated to form the large tier. +const String _complex = ''' +# Markdown rendering benchmark + +This is a **bold** paragraph with *italic*, `monospace`, ~~strike~~ and +[a link](https://example.com) mixed into a normal sentence. + +## Multiline paragraph + +Lorem ipsum dolor sit amet, +consectetur adipiscing elit. +Sed do eiusmod **tempor** incididunt +*ut labore* et dolore `magna aliqua`. + +### Quote + +> This is a simple quote. +> +> It may contain **several lines**, and even nested formatting like `code` +> or [links](https://example.com). + +### Code + +```javascript +function helloWorld() { + console.log("Hello, world!"); +} +``` + +Inline code works like this: `let x = 42;` + +### Lists + +- First item +- Second item with *italic* + - Subitem with **bold** + - Third level ~~strike~~ +- [x] Done task +- [ ] Pending task + +1. First step +2. Second step + 1. Substep 2.1 + 2. Substep 2.2 +3. Final step + +### Table + +| Name | Age | Role | +| :------ | --: | :--------: | +| Alice | 25 | Developer | +| Bob | 30 | Designer | +| Charlie | 35 | Manager | + +That is all for the *test* document. +'''; diff --git a/benchmark_compare/lib/styles.dart b/benchmark_compare/lib/styles.dart new file mode 100644 index 0000000..5676e70 --- /dev/null +++ b/benchmark_compare/lib/styles.dart @@ -0,0 +1,112 @@ +/// Shared, normalized styling so all three libraries render the corpus with the +/// same base text style, the same heading sizes and matched block spacing. This +/// removes layout *density* as a variable from the render/raster comparison โ€” +/// the whole point being that a more compact renderer otherwise rasterizes more +/// content per frame (see RESULTS.md). +/// +/// The key lever is an explicit line `height`: when set, every line box is +/// `fontSize * height`, independent of the font's own metrics โ€” so the three +/// libraries (and the headless-test vs profile-desktop environments) all lay +/// text out at the same vertical rhythm. +/// +/// Structural chrome that each library draws its own way โ€” code-block frames, +/// table borders, list bullets/indents, gpt_markdown's code header โ€” cannot be +/// unified through public style APIs and is left as-is; it is called out in the +/// height report. +library; + +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart' as fm; +import 'package:flutter_md/flutter_md.dart' as fmd; +import 'package:gpt_markdown/gpt_markdown.dart'; +import 'package:markdown/markdown.dart' as md; + +/// Base body text: identical for all three libraries. +const double kFontSize = 14; +const double kLineHeight = 1.4; +const Color kTextColor = Color(0xFF000000); + +const TextStyle kBaseTextStyle = TextStyle( + fontSize: kFontSize, + height: kLineHeight, + color: kTextColor, +); + +/// Heading sizes mirror flutter_md's scheme: base + {10, 8, 6, 4, 2, 0}, bold. +/// Index 0 == h1 โ€ฆ index 5 == h6. +const List _headingSizes = [24, 22, 20, 18, 16, 14]; + +TextStyle _heading(int levelIndex) => kBaseTextStyle.copyWith( + fontSize: _headingSizes[levelIndex], + fontWeight: FontWeight.bold, + ); + +/// Monospace body used for code, sized like the base text (no 0.85 shrink). +final TextStyle _monospace = kBaseTextStyle.copyWith(fontFamily: 'monospace'); + +/// Block gap: flutter_md inserts a spacer `fontSize` px tall per blank line, so +/// we match that for the libraries that expose a block-spacing knob. +const double kBlockSpacing = kFontSize; + +/// flutter_md theme. +fmd.MarkdownThemeData flutterMdTheme() => fmd.MarkdownThemeData( + textStyle: kBaseTextStyle, + textDirection: TextDirection.ltr, + textScaler: TextScaler.noScaling, + ); + +/// flutter_markdown style sheet. Starts from `fromTheme` (so every derived +/// style โ€” em/strong/del/a/checkbox โ€” exists and stays a delta merged onto the +/// base) and overrides the size/spacing-driving fields to match. +fm.MarkdownStyleSheet flutterMarkdownStyle(BuildContext context) => + fm.MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + p: kBaseTextStyle, + h1: _heading(0), + h2: _heading(1), + h3: _heading(2), + h4: _heading(3), + h5: _heading(4), + h6: _heading(5), + code: _monospace, + blockquote: kBaseTextStyle, + listBullet: kBaseTextStyle, + tableBody: kBaseTextStyle, + tableHead: kBaseTextStyle.copyWith(fontWeight: FontWeight.bold), + blockSpacing: kBlockSpacing, + textScaler: TextScaler.noScaling, + ); + +/// gpt_markdown theme override (headings matched to the shared scheme). The base +/// body style is passed to `GptMarkdown(style: kBaseTextStyle)` separately. +GptMarkdownThemeData gptMarkdownTheme(BuildContext context) => + GptMarkdownThemeData( + brightness: Theme.of(context).brightness, + h1: _heading(0), + h2: _heading(1), + h3: _heading(2), + h4: _heading(3), + h5: _heading(4), + h6: _heading(5), + ); + +/// The three library widgets, each built from `source` with the normalized +/// styles above. gpt_markdown needs a [BuildContext] for its inherited theme, +/// so every factory takes one (the others ignore it). +typedef StyledFactory = Widget Function(BuildContext context, String source); + +final Map styledLibraries = { + 'flutter_md': (context, src) => fmd.MarkdownWidget( + markdown: fmd.Markdown.fromString(src), + theme: flutterMdTheme(), + ), + 'flutter_markdown': (context, src) => fm.MarkdownBody( + data: src, + styleSheet: flutterMarkdownStyle(context), + extensionSet: md.ExtensionSet.gitHubFlavored, + shrinkWrap: true, + ), + 'gpt_markdown': (context, src) => GptMarkdownTheme( + gptThemeData: gptMarkdownTheme(context), + child: GptMarkdown(src, style: kBaseTextStyle), + ), +}; diff --git a/benchmark_compare/pubspec.yaml b/benchmark_compare/pubspec.yaml new file mode 100644 index 0000000..7fbc725 --- /dev/null +++ b/benchmark_compare/pubspec.yaml @@ -0,0 +1,50 @@ +# Head-to-head benchmark package: flutter_md vs flutter_markdown vs gpt_markdown. +# +# This is a standalone, non-published package that lives inside the repo purely +# to compare parsing and rendering performance across the three libraries. It is +# excluded from `flutter_md` publishing via the root `.pubignore`. +# +# flutter pub get +# dart run benchmark/parser_benchmark.dart # parser (benchmark_harness) +# flutter test test/render_benchmark_test.dart # rendering (widget tests) +name: md_benchmark_compare +description: "Head-to-head parse & render benchmarks: flutter_md vs flutter_markdown vs gpt_markdown." + +publish_to: "none" + +version: 0.0.1 + +environment: + sdk: ">=3.6.0 <4.0.0" + flutter: ">=3.29.0" + +dependencies: + flutter: + sdk: flutter + + # The library under test โ€” path dependency on the repo root. + flutter_md: + path: ../ + + # The two libraries we compare against. + flutter_markdown: ^0.7.7+1 # discontinued; the last published release + gpt_markdown: ^1.1.8 + + # The parser engine that flutter_markdown uses under the hood. We benchmark it + # directly so the parser comparison is apples-to-apples (flutter_markdown does + # no parsing of its own โ€” it delegates to this package). + markdown: ^7.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + benchmark_harness: ^2.3.1 + flutter_lints: ">=5.0.0 <7.0.0" + # Real profile-mode frame timing (flutter drive + TimelineSummary). + integration_test: + sdk: flutter + flutter_driver: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/benchmark_compare/test/render_benchmark_test.dart b/benchmark_compare/test/render_benchmark_test.dart new file mode 100644 index 0000000..a11f2e5 --- /dev/null +++ b/benchmark_compare/test/render_benchmark_test.dart @@ -0,0 +1,229 @@ +// RENDER BENCHMARK โ€” flutter_md vs flutter_markdown vs gpt_markdown. +// +// This measures the *end-to-end* cost of turning a Markdown **string** into +// painted pixels: parse + build + layout + paint, which is what every library +// actually does on the frame that first shows a message. It is measured as +// wall-clock time around `tester.pump()` after forcing a full subtree rebuild +// (a changing `ValueKey` unmounts the old tree and builds a fresh one). +// +// flutter test test/render_benchmark_test.dart +// +// All three libraries are given the SAME normalized styles (see lib/styles.dart) +// โ€” identical base text style + explicit line height, matched heading sizes and +// matched block spacing โ€” so layout *density* is no longer a variable. The test +// also prints a rendered-height table so the density match can be verified. +// +// Notes on fairness & interpretation: +// * Each iteration reconstructs from the source string. For flutter_md that +// means `Markdown.fromString(src)` is called inside the builder too, so its +// parse cost is included on equal footing with the other two (which parse +// inside build()). +// * All three render into the same fixed viewport (width 400) inside a +// SingleChildScrollView, so layout covers the whole document while paint is +// clipped to the same visible region for every library. +// * Absolute microseconds include fixed flutter_test frame overhead and are +// only meaningful *relative to each other* on the same machine โ€” use the +// ratios. Confirm real on-device frame time with the profile-mode scroll +// benchmark (integration_test/scroll_perf_test.dart). +// +// The reported number per (library, document) is the min-of-batches per-op time +// (the minimum is the most stable estimator, least perturbed by GC / scheduler). + +// ignore_for_file: avoid_print + +import 'dart:math' as math; + +import 'package:flutter/foundation.dart' show ValueListenable; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:md_benchmark_compare/corpus.dart'; +import 'package:md_benchmark_compare/styles.dart'; + +/// width the documents are laid out at. +const double _kWidth = 400; + +/// results[library][document] = us/op. +final Map> _results = >{ + for (final name in styledLibraries.keys) name: {}, +}; + +/// heights[library][document] = rendered px height (density check). +final Map> _heights = >{ + for (final name in styledLibraries.keys) name: {}, +}; + +const Key _probeKey = ValueKey('probe'); + +/// A host that rebuilds its subtree from scratch whenever [tick] changes, so a +/// pump measures the full parse+build+layout+paint of one library on one doc. +class _Host extends StatelessWidget { + const _Host({ + required this.tick, + required this.factory, + required this.source, + }); + + final ValueListenable tick; + final StyledFactory factory; + final String source; + + @override + Widget build(BuildContext context) => MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: ValueListenableBuilder( + valueListenable: tick, + builder: (context, value, _) => SingleChildScrollView( + child: SizedBox( + width: _kWidth, + // A fresh key each tick forces a full teardown + rebuild. + child: KeyedSubtree( + key: ValueKey(value), + child: Builder( + builder: (ctx) => KeyedSubtree( + key: _probeKey, + child: factory(ctx, source), + ), + ), + ), + ), + ), + ), + ), + ); +} + +Future _benchBuild( + WidgetTester tester, + ValueNotifier tick, { + int warmup = 4, + int batches = 10, + int itersPerBatch = 4, +}) async { + for (var i = 0; i < warmup; i++) { + tick.value++; + await tester.pump(); + } + var best = double.infinity; + for (var b = 0; b < batches; b++) { + final sw = Stopwatch()..start(); + for (var i = 0; i < itersPerBatch; i++) { + tick.value++; + await tester.pump(); + } + sw.stop(); + best = math.min(best, sw.elapsedMicroseconds / itersPerBatch); + } + return best; +} + +void main() { + for (final libEntry in styledLibraries.entries) { + final lib = libEntry.key; + final factory = libEntry.value; + + group('render: $lib', () { + for (final docEntry in renderCorpus.entries) { + final doc = docEntry.key; + final source = docEntry.value; + + testWidgets('$lib / $doc', (tester) async { + final tick = ValueNotifier(0); + await tester.pumpWidget( + _Host(tick: tick, factory: factory, source: source), + ); + // Surface any build-time error (e.g. an unsupported construct) loudly + // rather than recording a bogus timing. + expect(tester.takeException(), isNull, + reason: '$lib failed to render "$doc"'); + + _heights[lib]![doc] = tester.getSize(find.byKey(_probeKey)).height; + _results[lib]![doc] = await _benchBuild(tester, tick); + tick.dispose(); + }); + } + }); + } + + tearDownAll(_printTables); +} + +void _printTables() { + final docs = renderCorpus.keys.toList(); + final libs = styledLibraries.keys.toList(); + + _matrix('Render (end-to-end: parse + build + layout + paint), us/op โ€” ' + 'lower is better', docs, libs, _results, (v) => v.toStringAsFixed(1)); + + // Relative-to-flutter_md view (how many times slower each library is). + const base = 'flutter_md'; + print(''); + print('Render, relative to $base (x = library / $base; ' + 'lower is better, 1.00x = same speed)'); + _relative(docs, libs, _results, base); + + // Density check: rendered height per library, and its spread vs flutter_md. + _matrix('Rendered height (px) โ€” density check (styles normalized)', docs, + libs, _heights, (v) => v.toStringAsFixed(0)); + print(''); + print('Height relative to $base (x = library / $base; 1.00x = same height)'); + _relative(docs, libs, _heights, base); +} + +void _matrix( + String title, + List docs, + List libs, + Map> data, + String Function(double) fmt, +) { + print(''); + print(title); + print(''); + final header = StringBuffer('| document |'); + final sep = StringBuffer('| -------------- |'); + for (final lib in libs) { + header.write(' ${lib.padLeft(16)} |'); + sep.write(' ---------------: |'); + } + print(header); + print(sep); + for (final doc in docs) { + final row = StringBuffer('| ${doc.padRight(14)} |'); + for (final lib in libs) { + final v = data[lib]![doc]; + row.write(' ${(v == null ? '-' : fmt(v)).padLeft(16)} |'); + } + print(row); + } +} + +void _relative( + List docs, + List libs, + Map> data, + String base, +) { + print(''); + final header = StringBuffer('| document |'); + final sep = StringBuffer('| -------------- |'); + for (final lib in libs) { + header.write(' ${lib.padLeft(16)} |'); + sep.write(' ---------------: |'); + } + print(header); + print(sep); + for (final doc in docs) { + final baseVal = data[base]![doc]; + final row = StringBuffer('| ${doc.padRight(14)} |'); + for (final lib in libs) { + final v = data[lib]![doc]; + final cell = (baseVal == null || v == null || baseVal == 0) + ? '-' + : '${(v / baseVal).toStringAsFixed(2)}x'; + row.write(' ${cell.padLeft(16)} |'); + } + print(row); + } + print(''); +} diff --git a/benchmark_compare/test_driver/perf_driver.dart b/benchmark_compare/test_driver/perf_driver.dart new file mode 100644 index 0000000..6640eaa --- /dev/null +++ b/benchmark_compare/test_driver/perf_driver.dart @@ -0,0 +1,15 @@ +// Driver for the profile-mode scroll benchmark. +// +// `integrationDriver()` connects to the app launched by `flutter drive`, +// collects the `reportData` that `scroll_perf_test.dart` stored (one +// TimelineSummary per library) and writes the whole map to +// `build/integration_response_data.json`. +// +// Run: +// flutter drive \ +// --driver=test_driver/perf_driver.dart \ +// --target=integration_test/scroll_perf_test.dart \ +// --profile -d linux +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/benchmark_compare/tool/summarize_timeline.dart b/benchmark_compare/tool/summarize_timeline.dart new file mode 100644 index 0000000..43b36a2 --- /dev/null +++ b/benchmark_compare/tool/summarize_timeline.dart @@ -0,0 +1,114 @@ +// ignore_for_file: avoid_print +// +// Formats the profile-mode scroll benchmark output into a comparison table. +// +// Reads build/integration_response_data.json (written by the flutter drive run, +// one TimelineSummary per library) and prints Markdown tables of frame build & +// rasterizer times (average / 90th / 99th percentile) plus missed-frame counts. +// +// dart run tool/summarize_timeline.dart + +import 'dart:convert'; +import 'dart:io'; + +const String _kInput = 'build/integration_response_data.json'; + +// Report keys are 'scroll_' in feed order. +const List _libs = [ + 'flutter_md', + 'flutter_markdown', + 'gpt_markdown', +]; + +double? _num(Map m, String key) { + final v = m[key]; + if (v is num) return v.toDouble(); + return null; +} + +String _cell(double? v) => v == null ? '-' : v.toStringAsFixed(2); + +void main() { + final file = File(_kInput); + if (!file.existsSync()) { + stderr.writeln('Not found: $_kInput\n' + 'Run the profile-mode benchmark first:\n' + ' flutter drive --driver=test_driver/perf_driver.dart ' + '--target=integration_test/scroll_perf_test.dart --profile -d linux'); + exitCode = 1; + return; + } + + final data = json.decode(file.readAsStringSync()) as Map; + final summaries = >{}; + for (final lib in _libs) { + final raw = data['scroll_$lib']; + if (raw is Map) { + summaries[lib] = raw; + } else if (raw is String) { + // Some driver versions store the summary JSON as a string. + summaries[lib] = json.decode(raw) as Map; + } + } + + // Metrics to show: (label, json key, lower-is-better). + const metrics = >[ + ['frame build avg (ms)', 'average_frame_build_time_millis'], + ['frame build 90th (ms)', '90th_percentile_frame_build_time_millis'], + ['frame build 99th (ms)', '99th_percentile_frame_build_time_millis'], + ['frame build worst (ms)', 'worst_frame_build_time_millis'], + ['raster avg (ms)', 'average_frame_rasterizer_time_millis'], + ['raster 90th (ms)', '90th_percentile_frame_rasterizer_time_millis'], + ['raster 99th (ms)', '99th_percentile_frame_rasterizer_time_millis'], + ['raster worst (ms)', 'worst_frame_rasterizer_time_millis'], + ['missed build frames', 'missed_frame_build_budget_count'], + ['missed raster frames', 'missed_frame_rasterizer_budget_count'], + ['frame count', 'frame_count'], + ]; + + print(''); + print('Profile-mode scroll benchmark โ€” real frame timings (lower is better)'); + print(''); + final header = StringBuffer('| metric |'); + final sep = StringBuffer('| ----------------------- |'); + for (final lib in _libs) { + header.write(' ${lib.padLeft(16)} |'); + sep.write(' ---------------: |'); + } + print(header); + print(sep); + for (final metric in metrics) { + final row = StringBuffer('| ${metric[0].padRight(23)} |'); + for (final lib in _libs) { + final s = summaries[lib]; + final v = s == null ? null : _num(s, metric[1]); + row.write(' ${_cell(v).padLeft(16)} |'); + } + print(row); + } + print(''); + + // Relative build-time (the frame work each library asks of the UI thread), + // normalized to flutter_md. + final base = summaries['flutter_md']; + final baseBuild = + base == null ? null : _num(base, 'average_frame_build_time_millis'); + if (baseBuild != null && baseBuild > 0) { + print('Average frame build time relative to flutter_md ' + '(x = library / flutter_md):'); + print(''); + print('| ${'flutter_md'.padLeft(16)} | ' + '${'flutter_markdown'.padLeft(16)} | ${'gpt_markdown'.padLeft(16)} |'); + print('| ---------------: | ---------------: | ---------------: |'); + final row = StringBuffer('|'); + for (final lib in _libs) { + final s = summaries[lib]; + final v = + s == null ? null : _num(s, 'average_frame_build_time_millis'); + final cell = v == null ? '-' : '${(v / baseBuild).toStringAsFixed(2)}x'; + row.write(' ${cell.padLeft(16)} |'); + } + print(row); + print(''); + } +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..acff16e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,106 @@ +# Architecture + +`flutter_md` is four layers with a strict one-way dependency flow: + +``` +String โ”€โ”€โ–บ Parser โ”€โ”€โ–บ Node model โ”€โ”€โ–บ Render layer โ”€โ”€โ–บ Screen + โ”‚ โ–ฒ + โ””โ”€โ”€ Selection โ”€โ”˜ (anchored on the model, geometry from the render layer) +``` + +1. **Parser** (`lib/src/parser.dart`) turns a `String` into an immutable + `Markdown` (`lib/src/markdown.dart`) whose `blocks` are `MD$Block` nodes + (`lib/src/nodes.dart`). +2. **Render layer** (`lib/src/render/`, driven by `MarkdownWidget` in + `lib/src/widget.dart`) paints the model onto a canvas via one `RenderBox` and + a list of `BlockPainter`s โ€” there is no widget per block. +3. **Selection** (`lib/src/selection.dart`, `lib/src/selection_scope.dart`) holds + the selection as logical anchors over the immutable models, and reads geometry + from mounted render objects for hit-testing/highlight/handles. +4. **Theme** (`lib/src/theme.dart`) supplies styles and the customization hooks + (`builder`, `blockFilter`, `spanFilter`) that the render layer reads. + +The parser and node model are stable ground; the render layer was reorganized +into `lib/src/render/**` (see [rendering](rendering.md)). Selection is the newest +subsystem (issue #25). + +## Data flow, end to end + +`Markdown.fromString(src)` โ†’ `MarkdownDecoder.convert` โ†’ `Markdown{markdown, +blocks}`. You hand that `Markdown` to a `MarkdownWidget`: + +```dart +MarkdownWidget(markdown: Markdown.fromString(src), theme: myTheme) +``` + +`MarkdownWidget` (a `LeafRenderObjectWidget`) creates a `MarkdownRenderObject` +(a `RenderBox`), which owns a `MarkdownPainter`. The painter builds a +`List` (one per non-filtered block, via `theme.builder ??` +defaults), lays them out top-to-bottom recording each block's top-`y` in +`_blockOffsets`, and paints them into a cached `ui.Picture` keyed by size. + +For selection, the same widget opts in when given a `documentId` **and** a +resolvable `MarkdownSelectionController` (explicit or via an ambient +`MarkdownSelectionScope`). The render object then registers itself with the +controller as a `MarkdownSelectionSurface` and paints the selection highlight +(looked up from the controller) on top of the glyphs, so it stays visible over +opaque backgrounds. See [selection](selection.md). + +## Why the model is immutable and selection anchors to it + +Selection must span multiple blocks and multiple `MarkdownWidget`s (e.g. an +entire chat), including messages whose widgets are scrolled off and disposed by a +`ListView`. So the selection is stored as `(documentId, blockIndex, +renderedOffset)` over an app-supplied registry of immutable `Markdown` models, +not over render objects. Text extraction reads the models directly and works even +when nothing is mounted; only mounted surfaces contribute geometry (highlight +rects, handles). This is the core design decision; the alternatives (Flutter's +`SelectableRegion`, a custom selection delegate). + +## Load-bearing invariants + +These are cross-cutting; each subsystem doc repeats the ones it owns. + +- **Glyph cache.** `MarkdownPainter` caches one `ui.Picture` keyed by paint size; + it is reused on every repaint and only nulled by `update`/`invalidateLayout` + (model/theme change or system-font change). Repaints from selection or scroll + must not invalidate it. +- **Highlight outside the cache.** The selection highlight is drawn _before_ and + _outside_ the cached `Picture`, so drags/streaming repaint only the highlight. + `MarkdownRenderObject.isRepaintBoundary` is true whenever a controller is + attached, isolating those repaints. +- **Offset-space agreement.** A `SelectableBlockPainter`'s `renderedText` and + fragment offsets must equal `markdownBlockRenderedText(block)` (lists join items + with `\n`; tables join cells with `\t`, rows with `\n`; dividers/spacers are + empty). Hit-testing, highlight geometry, and copied text all index the same + space. `blockFilter` may drop blocks, but `_sourceIndices` preserves the true + `Markdown.blocks` index so anchors stay valid. +- **Span offsets.** Concatenating a block's `MD$Span.text` reproduces its rendered + text; `start/end` index that visible text. See [parser](parser.md) for the + escape/math/link caveats. + +## Public API surface + +Everything public is re-exported from `lib/flutter_md.dart`: + +- **Whole-file exports:** `markdown.dart`, `nodes.dart`, `parser.dart`, + `selection.dart`, `selection_scope.dart`, `theme.dart`, `widget.dart`. +- **Curated `show` from `render.dart`:** the `BlockPainter` framework + (`BlockPainter`, `SelectableBlockPainter`, `SelectableTextBlock`, + `MultiPainterSelectable`, `SelectableFragment`, `ParagraphGestureHandler`, + `paragraphFromMarkdownSpans`) and the nine default painters + (`BlockPainter$Paragraph โ€ฆ $Spacer`). + +Deliberately **not** public (reachable only via `import 'package:flutter_md/src/render.dart'`, +annotated `@meta.internal`): `MarkdownPainter`, `MarkdownRenderObject`. Treat the +`show` list as the supported surface; additions to it are permanent commitments. + +## Where to make a change + +- New/changed Markdown syntax โ†’ `parser.dart` (+ maybe `nodes.dart`); add + regression tests. See [parser](parser.md). +- Change how a block looks โ†’ a `BlockPainter$*` in `render/blocks/`, or supply + `MarkdownThemeData.builder` for a custom painter. See [rendering](rendering.md). +- Selection behavior (gestures, keyboard, extraction, streaming) โ†’ `selection.dart` + / `selection_scope.dart`. See [selection](selection.md). +- Tooling, CI, conventions โ†’ [development](development.md). diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..b14316a --- /dev/null +++ b/docs/development.md @@ -0,0 +1,133 @@ +# Development + +Commands, CI, conventions, benchmarks, and repo layout. Package `flutter_md` +(`0.2.0`); Dart `>=3.6.0 <4.0.0`, Flutter `>=3.29.0`. + +## Commands + +```shell +# Tests โ€” test/unit_test.dart is the single aggregate entrypoint. +flutter test test/unit_test.dart +flutter test --coverage --concurrency=40 test/unit_test.dart # CI form; LCOV โ†’ coverage/lcov.info + +# Analyzer โ€” INFO-level lints FAIL (--fatal-infos). A missing doc comment fails CI. +dart analyze --fatal-infos --fatal-warnings lib/ test/ + +# Format gate โ€” exactly as CI runs it (80 cols; the ! -name "*.*.dart" skips generated files): +find lib test -name "*.dart" ! -name "*.*.dart" -print0 \ + | xargs -0 dart format --set-exit-if-changed --line-length 80 -o none +dart format lib test example # to actually apply (page_width: 80) + +# Example app (separate package md_example, depends on flutter_md via path: ../): +cd example && flutter pub get && flutter run +``` + +### Benchmarks + +Parser benchmarks are pure Dart; the render benchmark needs the Flutter test +engine (real `dart:ui` text layout + `PictureRecorder`). + +```shell +dart run benchmark/parser_benchmark.dart # multi-scenario table + head-to-head vs `markdown` pkg +dart run benchmark/parse_benchmark.dart # single mixed doc vs Google `markdown` pkg +dart run benchmark/compare.dart --save # write baseline โ†’ benchmark/.baseline.txt (uncommitted) +dart run benchmark/compare.dart # compare current vs saved baseline, prints delta % +dart compile exe benchmark/parser_benchmark.dart -o /tmp/pb && /tmp/pb # stabler AOT numbers + +flutter test benchmark/render_benchmark.dart # vs benchmark/.render_baseline.txt +flutter test benchmark/render_benchmark.dart --dart-define=RENDER_BASELINE=save # record new baseline +``` + +Render tiers (doc = 50 mixed blocks, width 400): `layout_large`, `paint_miss` +(fresh painter โ†’ records a `Picture`), `paint_hit` (re-paint โ†’ replays cached +`Picture`), `stream_append` (`update()` + relayout each iter), `selection_drag` +(grow a highlight over the **reused** content Picture), `scroll_frame` (40 +drag+pump frames over a `ListView`). The test **asserts** `paint_hit < paint_miss` +and `selection_drag < paint_miss / 3` โ€” regressions fail. Timings are relative +(headless engine); confirm real FPS with `flutter run --profile` + DevTools. + +Parser scenarios live in `benchmark/scenarios.dart` (`prose, inline, links, lists, +table, code, quotes, escapes, currency, pathological, mixed`). `compare.dart` uses +warmup + auto-calibrated iterations + min-of-batches for low-noise deltas. + +## CI pipeline (`.github/workflows/`) + +**`checkout.yml`** (name `Checkout`) โ€” runs on push to `main`/`master` and PRs to +`main|master|dev|develop|feature/**|โ€ฆ`, path-filtered to Dart/config files. Single +job, steps in order (any non-zero fails): + +1. Checkout. +2. **Setup** (`./.github/actions/setup`): sparse-checkout, `flutter pub get`, and a + **CHANGELOG check** โ€” the `version:` in `pubspec.yaml` must appear as a + `# ` heading in `CHANGELOG.md`, else exit 1. +3. **Check code format** โ€” the format gate command above. +4. **Check analyzer** โ€” `dart analyze --fatal-infos --fatal-warnings lib/ test/`. +5. **Run unit tests** โ€” `flutter test --coverage --concurrency=40 test/unit_test.dart`. +6. Codecov upload (present but commented out). + +**`deploy.yml`** (name `Deploy to Pub.dev`) โ€” on `workflow_dispatch` and version +tags `[0-9]+.[0-9]+.[0-9]+*`; delegates to `dart-lang/setup-dart` publish workflow +(OIDC). Benchmarks are never run in CI. + +## Lint rules that bite (`analysis_options.yaml`) + +Base `package:flutter_lints/flutter.yaml`, plus `strict-casts`, `strict-raw-types`, +`strict-inference` (all true). The ones you'll actually trip on: + +- **`public_member_api_docs: true`** โ€” every public member needs a `///`. With + `--fatal-infos`, a missing doc fails CI. (`{@template}`/`{@macro}` is used for + boilerplate.) +- **`lines_longer_than_80_chars: true`** โ€” 80-col lint on top of the format gate. +- **`prefer_relative_imports: true`** (+ `avoid_relative_lib_imports: error`) โ€” + inside `lib/` imports are **relative** (`../nodes.dart`); tests use + `package:flutter_md/flutter_md.dart`. +- **`prefer_const_*` / `use_named_constants`** โ€” const is enforced. + +`errors:` overrides โ€” hard **errors**: `always_use_package_imports`, +`avoid_relative_lib_imports`, `avoid_slow_async_io`, `avoid_types_as_parameter_names`, +`valid_regexps`, `always_require_non_null_named_parameters`. **Ignored**: `todo`, +`curly_braces_in_flow_control_structures` (single-line `if` bodies allowed). +`example/analysis_options.yaml` is identical to root. + +## Conventions + +- **Relative imports inside `lib/`**; `package:` imports in `test/`. The barrel + `lib/flutter_md.dart` re-exports via `export 'src/โ€ฆ'`. +- **`$` separator** in public variant names โ€” `MD$Block`, `MD$Span`, + `BlockPainter$Paragraph`, even benchmark `Current$Benchmark`. Deliberate style. +- **`@meta.internal`** (imported `as meta show internal`) marks non-user-facing + types (`MarkdownPainter`, `MarkdownRenderObject`); `@protected` for subclass-only + members. Public model/nodes import `package:meta/meta.dart` plain. +- **One test entrypoint:** `test/unit_test.dart` imports each suite's `main()` + inside `group('Unit', โ€ฆ)`. A new `*_test.dart` must be wired there or CI skips it. +- **Doc comments required** on all public members (see the lint above). +- **CHANGELOG discipline:** bump `pubspec.yaml` `version:` and add the matching + `# ` heading in `CHANGELOG.md` together. + +## Dependencies + +- `dependencies`: `flutter` (sdk), `meta: ^1.16.0`. +- `dev_dependencies`: `flutter_test` (sdk), `flutter_lints: >=5.0.0 <7.0.0`, + `markdown: ^7.3.0` (benchmark comparison only), `benchmark_harness: ^2.3.1`. + +## Repo layout + +``` +lib/flutter_md.dart public barrel (export 'src/โ€ฆ' [+ show for render.dart]) +lib/src/ + markdown.dart nodes.dart parser.dart # model + parser โ†’ docs/parser.md + theme.dart # MarkdownThemeData, MarkdownTheme + widget.dart # MarkdownWidget (LeafRenderObjectWidget) + render.dart render/** # canvas render layer โ†’ docs/rendering.md + selection.dart selection_scope.dart # text selection โ†’ docs/selection.md +test/ + parser/ nodes/ selection/ theme/ widget/ (aggregated by test/unit_test.dart) +benchmark/ parser + render benchmarks, compare.dart, scenarios.dart, + .render_baseline.txt +example/ md_example app: lib/main.dart (Editor/Selection/Chat tabs), lib/tabs/* +AGENTS.md docs/ this documentation set +``` + +`build/`, `coverage/`, `.dart_tool/` are gitignored; both benchmark baselines +(`.baseline.txt` and `.render_baseline.txt`) are gitignored and uncommitted โ€” +they are machine-specific and generated locally. diff --git a/docs/migration/0.0.x-to-0.2.x.md b/docs/migration/0.0.x-to-0.2.x.md new file mode 100644 index 0000000..870f6c5 --- /dev/null +++ b/docs/migration/0.0.x-to-0.2.x.md @@ -0,0 +1,380 @@ +# Migrating `flutter_md` from 0.0.x to 0.2.x + +This guide walks you from the **0.0.x** line (last release: `0.0.8`) to the +**0.2.x** line (`0.2.0`). It covers everything that landed across `0.1.0` and +`0.2.0` so you can upgrade in a single step. + +> **TL;DR** โ€” 0.2.x is **almost entirely backward compatible**. Most apps +> upgrade with **zero code changes**. There is exactly **one** compile-time +> breaking change (a new `MD$Alert` branch in the block model) and a handful of +> parser bug-fixes that change output only for previously-malformed edge cases. +> No SDK, Flutter, or dependency version changes are required. + +--- + +## Contents + +- [Is this a hard migration?](#is-this-a-hard-migration) +- [Step 1 โ€” Bump the version](#step-1--bump-the-version) +- [Step 2 โ€” Handle the new `MD$Alert` block *(the only required change)*](#step-2--handle-the-new-mdalert-block-the-only-required-change) +- [Step 3 โ€” Review parser behaviour changes](#step-3--review-parser-behaviour-changes) +- [Step 4 โ€” Adopt the new features *(optional)*](#step-4--adopt-the-new-features-optional) +- [New public API reference](#new-public-api-reference) +- [Compatibility matrix](#compatibility-matrix) +- [FAQ / Troubleshooting](#faq--troubleshooting) + +--- + +## Is this a hard migration? + +No. The public surface from 0.0.8 is **fully additive** in 0.2.x with a single +exception. Here is the whole story in one table: + +| Concern | Changed in 0.2.x? | Action needed | +| --- | --- | --- | +| Dart / Flutter SDK constraints | โŒ No (`sdk >=3.6.0 <4.0.0`, `flutter >=3.29.0`) | None | +| `dependencies` in `pubspec.yaml` | โŒ No | None | +| `MarkdownWidget(markdown:, theme:)` | โŒ No โ€” same constructor | None | +| `Markdown.fromString(String)` | โŒ No โ€” same call | None | +| `MarkdownThemeData(...)` fields | โž• Additive (new optional fields) | None | +| `MD$Block.map(...)` exhaustive calls | โš ๏ธ **Breaking** โ€” new `alert` branch | [Step 2](#step-2--handle-the-new-mdalert-block-the-only-required-change) | +| `switch (block)` over the sealed model | โš ๏ธ **Breaking** โ€” new `MD$Alert` case | [Step 2](#step-2--handle-the-new-mdalert-block-the-only-required-change) | +| `MD$Block.maybeMap(...)` | โœ… Safe (new branch is optional, falls to `orElse`) | Optional | +| Parser output for edge cases | โš ๏ธ A few bug-fixes | [Step 3](#step-3--review-parser-behaviour-changes) | +| Everything else (theme, spans, painters) | โž• Additive | None | + +If your app only ever calls `MarkdownWidget(markdown: Markdown.fromString(...))` +and configures a `MarkdownThemeData`, you can upgrade by bumping the version and +you are done. Read [Step 3](#step-3--review-parser-behaviour-changes) anyway โ€” +it is short and explains subtle rendering differences you might notice. + +--- + +## Step 1 โ€” Bump the version + +```yaml +dependencies: + flutter_md: ^0.2.0 +``` + +```bash +flutter pub get +``` + +Nothing else in `pubspec.yaml` needs to change โ€” the SDK/Flutter constraints and +the (empty) runtime dependency set are identical to 0.0.8. + +--- + +## Step 2 โ€” Handle the new `MD$Alert` block *(the only required change)* + +0.1.0 introduced GitHub-style **alert** blocks (`> [!NOTE]`, `> [!TIP]`, +`> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]`). They are a new subtype of the +sealed `MD$Block` model: + +```dart +final class MD$Alert extends MD$Block { ... } +``` + +Because `MD$Block` is a **sealed** class and `MD$Block.map()` takes a +**required** callback per subtype, adding `MD$Alert` is a breaking change for +any code that visits blocks exhaustively. You will hit this only if you traverse +the model yourself (custom rendering, block filtering that inspects types, +serialization, analytics, etc.). Rendering through `MarkdownWidget` needs no +change. + +### 2a. If you call `MD$Block.map(...)` + +Add an `alert:` branch. This is a **compile error** until you do. + +```dart +// Before (0.0.8) โ€” compiles. +block.map( + paragraph: (p) => ..., + heading: (h) => ..., + quote: (q) => ..., + code: (c) => ..., + list: (l) => ..., + divider: (d) => ..., + table: (t) => ..., + spacer: (s) => ..., +); +``` + +```dart +// After (0.2.0) โ€” add the `alert` branch. +block.map( + paragraph: (p) => ..., + heading: (h) => ..., + quote: (q) => ..., + code: (c) => ..., + list: (l) => ..., + divider: (d) => ..., + table: (t) => ..., + alert: (a) => ..., // ๐Ÿ‘ˆ new + spacer: (s) => ..., +); +``` + +### 2b. If you `switch` over the sealed type + +Add a `case MD$Alert`. The analyzer flags the switch as non-exhaustive until you +do. + +```dart +switch (block) { + case MD$Paragraph p: ... + case MD$Heading h: ... + case MD$Quote q: ... + case MD$Alert a: ... // ๐Ÿ‘ˆ new + case MD$Code c: ... + case MD$List l: ... + case MD$Divider d: ... + case MD$Table t: ... + case MD$Spacer s: ... +} +``` + +### 2c. If you use `MD$Block.maybeMap(...)` + +**No change required.** `maybeMap`'s new `alert` parameter is optional and +routes to your `orElse` when omitted, so existing calls keep compiling and +behaving as before. Add an explicit `alert:` only if you want to handle alerts +specially. + +### 2d. Note: `> [!NOTE]` blockquotes are now `MD$Alert`, not `MD$Quote` + +Previously a blockquote whose first line was `> [!NOTE]` parsed as an ordinary +`MD$Quote` (with the literal `[!NOTE]` text inside). It now parses as an +`MD$Alert` with `alert.alert == MD$AlertType.note` and the marker line removed +from the body. If you special-cased those quotes by inspecting their text, move +that logic to the `MD$Alert` branch. + +--- + +## Step 3 โ€” Review parser behaviour changes + +0.1.0 tightened the parser to follow CommonMark-inspired flanking rules and fix +long-standing edge cases. These change **output**, but only for input that was +previously mis-parsed. Skim the table; if none of these patterns appear in your +content, there is nothing to do. + +| Input | 0.0.8 result | 0.2.x result | Why | +| --- | --- | --- | --- | +| `5 * 6 = 30` | `6 ` italicised (stray `*`) | Literal text | Emphasis requires proper flanking | +| `**bold never closed` | Leaked bold to line end | Literal `**bold never closed` | Unterminated markers stay literal | +| `snake_case`, `object_id` | `_` treated as emphasis | Preserved verbatim | Intraword `_` is not emphasis | +| `#hashtag` | Treated as heading | Literal paragraph text | ATX headings need a space after `#` | +| `####### too many` (7+ `#`) | Heading | Literal text | Max heading level is 6 | +| `## Title ##` | Trailing `##` kept | Trailing `#` stripped | ATX closing sequence removed | +| `***` / `___` (and `- - -`) | Not always a rule | Thematic break (`MD$Divider`) | New rule variants | +| ` ~~~ ` fenced block | Not recognized | Fenced code block (like ` ``` `) | New fence marker | +| `\$` | Backslash + `$` | Literal `$` | `\$` is now a recognized escape | +| `*[link](url)*` (emphasis around a link) | Split spans | Emphasis merged onto the link span | Cleaner span model | +| `[t]()`, `[t](url 'title')` | Not parsed | Angle-bracket URLs & single-quoted titles supported | Wider link syntax | + +There is also a small fix worth knowing about: **`MarkdownThemeData.copyWith` +now preserves `builder` and `onLinkTap`.** In 0.0.8 those two were silently +dropped by `copyWith`; if you relied on that (accidental) behaviour to clear +them, pass them explicitly instead. + +> **Inline math is opt-in and off by default**, so the default parse of `$5`, +> `$HOME`, or `$x$` is unchanged. See [Step 4](#inline-math-opt-in) to enable +> it. + +--- + +## Step 4 โ€” Adopt the new features *(optional)* + +None of these are required, but they are the reason to upgrade. + +### Text selection (headline feature of 0.2.0) + +Cross-block **and** cross-widget (chat) text selection, anchored on the +immutable model so it survives list scrolling/disposal and streaming updates. +Opt in per group of widgets: + +```dart +final controller = MarkdownSelectionController(); + +// Register documents in reading order (a chat feeds this from its list). +controller.setDocuments([ + for (final (i, m) in messages.indexed) + MarkdownDocumentRef(id: m.id, model: m.markdown, order: i), +]); + +MarkdownSelectionScope( + controller: controller, + child: ListView.builder( + itemCount: messages.length, + itemBuilder: (context, i) => MarkdownWidget( + documentId: messages[i].id, // ๐Ÿ‘ˆ opt in + markdown: messages[i].markdown, + ), + ), +); + +final String text = controller.getText(); // default formatter +final structured = controller.selectedContent(); // per-document/-block +``` + +A `MarkdownWidget` with **no** `documentId`/`controller` is completely inert, so +this is fully opt-in. Selection ships with keyboard shortcuts, native handles + +magnifier on touch, a context toolbar, and word/block granularity. See the +[Text Selection section of the README](../../README.md#-text-selection) and the +runnable **Selection** / **Chat** tabs in `example/`. + +### GitHub alerts + +```markdown +> [!WARNING] +> Critical content demanding immediate user attention. +``` + +Style per-type accent colours through the theme: + +```dart +MarkdownThemeData( + alertColors: const { + MD$AlertType.warning: Color(0xFF9A6700), + }, +); +``` + +Missing entries fall back to the GitHub default palette via `alertColorFor`. + +### Task lists + +`- [x]` / `- [ ]` items render with a checkbox and expose their state on the +model: + +```dart +if (item.isTask) { + final done = item.checked; // true / false +} +``` + +### Table column alignment + +Delimiter-row alignment (`:---`, `:--:`, `---:`) is captured on the model and +applied when rendering: + +```dart +final align = table.alignmentFor(columnIndex); // MD$TableColumnAlign.left/center/right/none +``` + +### Link styling + +Merge a custom `TextStyle` on top of the default link styling: + +```dart +MarkdownThemeData( + linkColor: Colors.indigo, + linkStyle: const TextStyle(decoration: TextDecoration.underline), +); +``` + +### Inline math (opt-in) + +Off by default. Enable per parse or via a reusable decoder: + +```dart +// Per parse: +final md = Markdown.fromString(r'The angle $\alpha$ and $x^2$.', inlineMath: true); + +// Reusable decoder, optionally extending the command table: +const decoder = MarkdownDecoder( + inlineMath: true, + mathReplacements: {...kMarkdownMathCommands, r'\R': 'โ„'}, +); +``` + +It converts common LaTeX commands and super/subscripts to Unicode, is +code-span/code-block safe, and treats `\$` as a literal dollar. + +--- + +## New public API reference + +Everything below is **new in 0.2.x** and purely additive (except the `MD$Alert` +branch noted in [Step 2](#step-2--handle-the-new-mdalert-block-the-only-required-change)). + +**Node model (`nodes.dart`)** +- `MD$Alert` block, `MD$AlertType` enum (`marker`, `title`, `tryParse`) +- `MD$TableColumnAlign` enum; `MD$Table.alignments`, `MD$Table.alignmentFor` +- `MD$ListItem.checked`, `MD$ListItem.isTask` +- `MD$Block.map` / `maybeMap` gain an `alert` branch + +**Theme (`theme.dart`)** +- `MarkdownThemeData.linkStyle` +- `MarkdownThemeData.alertColors`, `alertColorFor` +- `copyWith` now preserves `builder` and `onLinkTap` + +**Decoder (`markdown.dart` / `parser.dart`)** +- `MarkdownDecoder({inlineMath, mathReplacements})` +- `Markdown.fromString(text, {inlineMath})` +- `kMarkdownMathCommands` + +**Widget (`widget.dart`)** +- `MarkdownWidget.controller`, `MarkdownWidget.documentId` + +**Selection (`selection.dart` / `selection_scope.dart`)** +- `MarkdownSelectionController`, `MarkdownSelectionScope` (+ `MarkdownSelectionScopeState`) +- `MarkdownSelectionGroup`, `MarkdownDocumentRef` +- `MarkdownPosition`, `MarkdownSelection` +- `MarkdownSelectedContent` (+ document/block variants) +- `MarkdownSelectionFormatter`, `MarkdownPlainTextFormatter` +- `MarkdownReconciliationPolicy`, `MarkdownSelectionSurface` +- `MarkdownHandleEndpoints`, `markdownBlockRenderedText` + +**Render framework (`render.dart`, additive exports)** +- `SelectableBlockPainter`, `SelectableTextBlock` +- `MultiPainterSelectable`, `SelectableFragment`, `ParagraphGestureHandler` +- `paragraphFromMarkdownSpans` +- Default painters `BlockPainter$Paragraph โ€ฆ BlockPainter$Spacer` + +--- + +## Compatibility matrix + +| Version | `MarkdownWidget` API | Node model | Requires code change from 0.0.8? | +| --- | --- | --- | --- | +| 0.0.8 | `markdown`, `theme` | 9 block types | โ€” | +| 0.1.0 | same | +`MD$Alert`, task lists, table alignment | Only exhaustive `map`/`switch` callers | +| 0.2.0 | +`controller`, `documentId` (optional) | same as 0.1.0 | Same as 0.1.0 | + +--- + +## FAQ / Troubleshooting + +**Q: My build fails with "missing `alert` argument" / "switch is not +exhaustive" after upgrading.** +You call `MD$Block.map(...)` or `switch` over `MD$Block` directly. Add the +`alert` branch / `case MD$Alert`. See +[Step 2](#step-2--handle-the-new-mdalert-block-the-only-required-change). + +**Q: A blockquote that used to render as a quote now looks different.** +If its first line is `> [!NOTE]` (or TIP/IMPORTANT/WARNING/CAUTION) it is now an +alert block. This is intentional โ€” see +[Step 2](#step-2--handle-the-new-mdalert-block-the-only-required-change). + +**Q: Some emphasis/headings render as literal text now.** +That is the parser correctness pass from +[Step 3](#step-3--review-parser-behaviour-changes). The old output was a bug; +the new output matches CommonMark-style rules. + +**Q: `$`-prefixed text (prices, shell vars) โ€” will inline math eat it?** +No. Inline math is **disabled by default**. Nothing changes unless you pass +`inlineMath: true`. + +**Q: Do I have to use text selection?** +No. It is opt-in. A `MarkdownWidget` without a `documentId`/`controller` behaves +exactly as in 0.0.8. + +**Q: Did the SDK / dependency requirements change?** +No. `sdk: >=3.6.0 <4.0.0`, `flutter: >=3.29.0`, and no added runtime +dependencies. + +--- + +For the full, dated list of changes see the [CHANGELOG](../../CHANGELOG.md). diff --git a/docs/parser.md b/docs/parser.md new file mode 100644 index 0000000..e90b1c1 --- /dev/null +++ b/docs/parser.md @@ -0,0 +1,164 @@ +# Parser & node model + +Files: `lib/src/parser.dart`, `lib/src/nodes.dart`, `lib/src/markdown.dart`. +The parser is stable, hot-path-optimized ground โ€” behavior is locked by +`test/parser/**` (especially `regression_test.dart`). Preserve edge semantics. + +## Entry points + +Three ways from `String` to `Markdown`: + +```dart +Markdown.fromString(src); // convenience factory; inlineMath: false +markdownDecoder.convert(src); // shared const MarkdownDecoder() +const MarkdownDecoder(inlineMath: true).convert(src); +``` + +- `MarkdownDecoder extends Converter` โ€” `const + MarkdownDecoder({bool inlineMath = false, Map? mathReplacements})`. + The whole parse is one `convert(String)` method: a hand-rolled line loop, + branched on the **first code unit** of each line so plain prose runs zero + regexes. Being a `dart:convert` `Converter`, it also gets `.fuse`/`.cast`/streaming. +- `Markdown` โ€” `final class` with `String markdown` (original source), + `List blocks` (unmodifiable), `isEmpty`/`isNotEmpty`, and a lossy + `String get text` (concatenated span text; expensive; tests only). `toString()` + returns the raw source. Parsing is relatively expensive โ€” do it outside build. + +## Node model (`nodes.dart`) + +`sealed class MD$Block` is the base of every block (`@immutable`), with abstract +`String type` and `String text`. Dispatch is a **`map`/`maybeMap` pattern**, not a +visitor object: + +```dart +block.map( + paragraph: (p) => โ€ฆ, heading: (h) => โ€ฆ, quote: (q) => โ€ฆ, code: (c) => โ€ฆ, + list: (l) => โ€ฆ, divider: (d) => โ€ฆ, table: (t) => โ€ฆ, alert: (a) => โ€ฆ, spacer: (s) => โ€ฆ, +); +// maybeMap({... optional, required orElse}) fills unset handlers with orElse. +``` + +Because `MD$Block` is `sealed`, an exhaustive `switch` also works. **There are +nine block kinds โ€” no image block** (`MD$Image` exists only commented-out); images +are inline spans. Adding a block kind means extending the `map`/`maybeMap` +signature โ€” a breaking change to every override. + +| Block | `type` | Key fields | +|---|---|---| +| `MD$Paragraph` | `paragraph` | `text`, `List spans` | +| `MD$Heading` | `heading` | `int level` (1โ€“6), `text`, `spans` | +| `MD$Quote` | `quote` | `int indent` (**always 1** โ€” nested `>>` not modeled yet), `text`, `spans` | +| `MD$Alert` | `alert` | `MD$AlertType alert`, `text` (body), `spans` (body only) | +| `MD$Code` | `code` | `String? language` (may be `''`), `text` (raw, never span-parsed) | +| `MD$List` | `list` | `text` (raw slice), `List items` | +| `MD$Divider` | `divider` | none; `text == '---'` | +| `MD$Table` | `table` | `MD$TableRow header`, `List rows`, `List alignments` + `alignmentFor(i)` | +| `MD$Spacer` | `spacer` | `int count` (collapsed blank lines); `text == '\n' * count` | + +Supporting types: + +- **`MD$Span`** โ€” `int start`, `int end`, `String text`, `MD$Style style`, + `Map? extra`. `extra` carries link/image metadata: `'type'` + (`link`/`image`), `'href'`/`'src'`, `'url'`, optional `'alt'`. +- **`MD$Style`** โ€” a **bitmask**, `extension type const MD$Style(int value) + implements int` (not an enum). Flags: `none`, `italic`, `bold`, `underline`, + `strikethrough`, `monospace`, `link`, `image`, `highlight`, `spoiler`. Combine + with `|`/`.add`; query with `.contains`. The parser stores combined masks + (e.g. bold|link). +- **`MD$ListItem`** โ€” `int indent` (leading-space **column width**, not a level + ordinal), `bool? checked` (`null` = not a task; `false`/`true` = task state), + `bool isTask`, `String marker` (literal source marker: `-`,`*`,`+`,`1.`,`1)`), + `text`, `spans`, `List children`, `copyWith(...)`. +- **`MD$TableRow`** โ€” `text`, `List> cells` (each cell its own span list). +- **`MD$TableColumnAlign`** โ€” `none, left, center, right`. +- **`MD$AlertType`** โ€” `note, tip, important, warning, caution`, each with a + `marker` and a `title` (`Note`, `Tip`, โ€ฆ), plus `static tryParse(keyword)` + (case-insensitive). + +## The span offset invariant + +Every `MD$Span.start/end` are **UTF-16 offsets into its block's `text`**, and +**`spans.map((s) => s.text).join()` reproduces the block's rendered (visible) +text** โ€” markers, escapes, and link/image syntax removed. Locked by +`test/parser/regression_test.dart` ("Span offset invariants"): + +- spans are ordered by ascending `start`; every span has `start <= end`; +- plain text with no markers is a single span `start=0`, `end=text.length`, + `style=none`. + +Selection depends on this to map a visible-text range back to positions/styles. +`start/end` index the **visible** text, so they can diverge from raw-source +offsets: + +- **Escapes:** the backslash is removed from `text`; `end` is reduced by the + number of removed backslashes, so `end - start != text.length`. +- **Inline math** (when enabled): offsets index the already-substituted Unicode + string, not the `$โ€ฆ$` source. +- **Links/images:** offsets span the full `[..](..)` / `![..](..)` source range + while `text` is only the label, so `text.length != end - start` there. + +## GFM support & intentional/nonstandard choices + +Emphasis (CommonMark-inspired flanking rules; stray/unterminated markers stay +literal and never leak style to end of line): + +- `*x*` italic, `**x**` bold; `_x_` italic, **`__x__` = UNDERLINE, not bold**. +- `~~x~~` strikethrough, `==x==` highlight, `||x||` spoiler โ€” all require the + **double** marker; a single one is literal. +- `` `x` `` monospace, single backtick only (double backtick unsupported); inside + a code span all markdown is literal. +- `_` cannot open/close intra-word (snake_case, Cyrillic preserved); `*` may + emphasize intra-word (`a*b*c`). + +Inline: + +- **Soft line breaks preserved** โ€” consecutive non-blank lines join into one + paragraph with embedded `\n`; a blank line splits paragraphs and emits a + `MD$Spacer`. +- **Escapes** for `` ! # $ ( ) * + - . [ \ ] _ ` { } ``; `\\` โ†’ `\`. +- **Links/images** `[text](url)`, `![alt](src)`; angle-bracket URLs, quoted/paren + titles, balanced parens in bare URLs. **No reference-style links, no + autolinks.** Emphasis wrapping a link merges masks (`**[x](u)**` โ†’ bold|link). +- **Inline math `$โ€ฆ$` is opt-in** (`inlineMath: false` by default, so `$5`, + `$HOME` are untouched). When on, only `$โ€ฆ$` runs containing a recognized + `\command` or `^`/`_` script convert to Unicode; currency and unmappable runs + stay literal; inline/fenced code is protected; `\$` opts out. Command table is + `kMarkdownMathCommands` (public, extensible via the `mathReplacements` ctor arg). + +Block-level: + +- **Task lists** `- [ ]` / `- [x]` / `- [X]` (also ordered); empty `[]` is not a task. +- **Tables** with a delimiter row encoding per-column alignment (`:--`, `:-:`, + `--:`); malformed/ragged tables fall back to paragraph. +- **Alerts** โ€” a blockquote whose first line is `[!NOTE|TIP|IMPORTANT|WARNING|CAUTION]`; + unknown markers fall back to a plain quote. +- **Thematic breaks** `---`/`***`/`___` (3+ markers); **ATX headings** `#`โ€“`######` + (`#hashtag` and 7+ `#` are paragraphs; trailing `#` stripped); **fenced code** + ` ``` ` / `~~~` with a language label; unterminated fences run to EOF. + +## Gotchas for contributors + +- `convert` gates on the first code unit; perf-critical scans (`_isBlank`, + `_parseListLine`, `_parseLinkTarget`, escape range-copy, inline-math bails) were + rewritten from regexes โ€” `regression_test.dart` locks their exact semantics. +- The **inline fast path** returns a single unstyled full-width span when the text + contains none of `` * _ ~ = | \ [ ` ``. Any new inline syntax must extend that + special-character set or it becomes invisible to the parser. +- List `indent` is a **column width**, capped at 8; 9+ leading spaces end the list. + Tree assembly is a recursive `traverse` over a flat list with a shared mutable + offset closure (note the intermediate record misspells the field `intent`). +- `MD$Quote.indent` is hardcoded to 1; alert bodies are trimmed (`skip(1).join('\n').trim()`) + while quote bodies keep raw `join('\n')`. +- `MD$Code.language` can be `''` (not null) for a bare fence; `MD$Code.text` is raw. +- `MD$Table.alignments` may be shorter than the column count โ€” always use + `alignmentFor(i)`, never index directly. +- `blocks` and most nested lists (`items`, `rows`, `cells`, `alignments`) are + unmodifiable โ€” don't mutate in place. +- Empty input โ†’ `const Markdown.empty()`; a doc ending in blank lines still emits a + trailing `MD$Spacer`. + +## Public API (via `flutter_md.dart`) + +All of `markdown.dart` (`Markdown`), `nodes.dart` (every `MD$*`, `MD$Style`, +`MD$AlertType`, `MD$TableColumnAlign`, `MD$ListItem`, `MD$TableRow`, `MD$Span`), +and `parser.dart` (`MarkdownDecoder`, `markdownDecoder`, `kMarkdownMathCommands`). diff --git a/docs/rendering.md b/docs/rendering.md new file mode 100644 index 0000000..9a3869b --- /dev/null +++ b/docs/rendering.md @@ -0,0 +1,222 @@ +# Rendering layer + +Files: `lib/src/render.dart` (a re-export barrel), `lib/src/render/**`, +`lib/src/theme.dart`, `lib/src/widget.dart`. The layer paints the model onto a +canvas via one `RenderBox` and a list of `BlockPainter`s โ€” **no widget per block**. + +## File map (`lib/src/render/`) + +`render.dart` is a **barrel** (`library;` + `export` lines) so legacy +`import 'src/render.dart'` keeps resolving; it has no code. + +| File | Contents | +|---|---| +| `block_painter.dart` | The framework: `BlockPainter`, `SelectableBlockPainter`, mixins `SelectableTextBlock` / `MultiPainterSelectable` / `ParagraphGestureHandler`, class `SelectableFragment`, private `_distanceToRect`. | +| `span_builder.dart` | `paragraphFromMarkdownSpans({spans, theme, textStyle})` โ†’ `TextSpan`; private `_buildTapRecognizer` (link taps from `span.extra['url']`). | +| `markdown_painter.dart` | `MarkdownPainter` (`@meta.internal`) โ€” the orchestrator. | +| `markdown_render_object.dart` | `MarkdownRenderObject` (`@meta.internal`) โ€” the `RenderBox`, also a `MarkdownSelectionSurface`; plus `_paintNothing`. | +| `blocks/*.dart` | `BlockPainter$Paragraph, $Heading, $Quote, $Alert, $Code, $List` (+ private `_ListItemMetrics`), `$Table`, `$Divider`, `$Spacer`. | + +## Render flow + +`MarkdownWidget` (`LeafRenderObjectWidget`) โ†’ `createRenderObject` builds a +`MarkdownRenderObject` then `updateSelection(controller, documentId)`; +`updateRenderObject` calls `update(markdown, theme)` + `updateSelection(...)`. +Theme resolution: explicit `theme` โ†’ `MarkdownTheme.maybeOf(context)` โ†’ a default +from `DefaultTextStyle`/`Directionality`/`MediaQuery.textScaler`. + +`MarkdownRenderObject` (a `RenderBox`, not `sizedByParent`) owns one +`MarkdownPainter`. `computeDryLayout`/`performLayout` both do +`constraints.constrain(_painter.layout(maxWidth: constraints.maxWidth))`. + +`MarkdownPainter`: + +- **Build** (`_rebuild`): reads `theme.blockFilter` and `theme.builder ?? + _defaultBlockBuilder`; for each non-filtered block appends + `builder(block, theme) ?? _defaultBlockBuilder(block, theme)` to + `_blockPainters`, records the true `Markdown.blocks` index in `_sourceIndices`, + sizes `_blockOffsets`. `_defaultBlockBuilder` is a `block.map(...)` to the nine + `BlockPainter$*` constructors. +- **Layout:** per-block, top-to-bottom. Writes each block's top-`y` into + `_blockOffsets[i]`, calls `block.layout(maxWidth)`, accumulates height, tracks + max width. **No inter-block spacing** is added โ€” a `MD$Spacer` block supplies gaps. +- **Paint:** guarded by `!_needsLayout`. **Glyphs are cached in a `ui.Picture` + keyed by size** โ€” if `_lastSize == size`, the picture is replayed via + `drawPicture`; otherwise it re-records (walking blocks, `painter.paint($canvas, + size, offset)`, advancing `offset += painter.size.height`). The cache is nulled + only by `update` (model/theme change) and `invalidateLayout` (system fonts). An + overflow guard stops emitting blocks past the viewport height. +- **Hit-testing** (`_blockIndexForDy`): **binary search** over `_blockOffsets`. + `positionForLocal` maps a content-local offset โ†’ `(sourceBlockIndex, textOffset)` + (null if the hit block isn't a `SelectableBlockPainter`). `handleEvent` routes + tap-down/up to the block, re-basing the pointer into block-local space (a manual + `PointerEvent` clone, since `PointerEvent` has no `copyWith`). +- **System fonts:** `MarkdownRenderObject.attach` listens on + `PaintingBinding.instance.systemFonts`; a change calls `invalidateLayout` (nulls + the cache, disposes+rebuilds every block painter so `TextPainter`s re-layout) + + `markNeedsLayout`. + +## The `BlockPainter` framework + +`BlockPainter` โ€” every painter implements this: + +```dart +abstract final Size size; // valid only after layout() +void handleTapDown(PointerDownEvent event); // block-LOCAL coords +void handleTapUp(PointerUpEvent event); +Size layout(double width); // measure at width; sets size +void paint(Canvas canvas, Size size, double offset); // size = full content size; offset = block top-y +void dispose(); +``` + +Coordinate contract: taps arrive in **block-local** space; `paint`'s `offset` is +the block's top-`y` in the content, and `size` is the full content size (blocks +use `size.width` for full-width backgrounds/rules and to bail when too narrow). + +`SelectableBlockPainter implements BlockPainter` adds selection: + +```dart +String get renderedText; // MUST equal markdownBlockRenderedText(block) +int offsetForLocalPosition(Offset local); // block-local โ†’ rendered-text index +List boxesForRange(int start, int end);// block-local highlight rects for [start, end) +``` + +Two mixins implement it for you: + +- **`SelectableTextBlock`** โ€” a block backed by a **single `TextPainter`**. Supply + `TextPainter get selectionPainter` and optionally override `Offset get + selectionOrigin` (default `Offset.zero`) when glyphs aren't at the block origin. + It derives `renderedText`, `offsetForLocalPosition`, `boxesForRange` for you. +- **`MultiPainterSelectable`** โ€” a block whose text spans **several + `TextPainter`s** at different origins (lists, tables). Supply + `List get fragments` (in rendered-text order, each + `textStart` matching the linearization; the gaps are the `\n`/`\t` separators) + and `renderedText`. `SelectableFragment` is `(TextPainter painter, Offset + origin, int textStart)`. + +`ParagraphGestureHandler` โ€” mix in for link taps: `hitTestInlineSpanWithPointerEvent(event, +painter)` resolves the `InlineSpan` under a pointer so `handleTapDown`/`handleTapUp` +can match down and up on the same span before firing its recognizer. + +`paragraphFromMarkdownSpans({spans, theme, textStyle})` โ†’ `TextSpan` โ€” the public +helper that applies `theme.spanFilter`, maps each `MD$Span` via +`theme.textStyleFor(span.style)` (merged under `textStyle` if given), and attaches +a link recognizer (from `theme.onLinkTap` + `span.extra['url']`). **Use it** for +any custom text block so filters/styles/link-taps stay consistent. + +### Recipe: a custom block painter + +```dart +class MyBlock with ParagraphGestureHandler, SelectableTextBlock implements BlockPainter { + MyBlock({required List spans, required this.theme}) + : painter = TextPainter( + text: paragraphFromMarkdownSpans(spans: spans, theme: theme), + textDirection: theme.textDirection, textScaler: theme.textScaler); + + final MarkdownThemeData theme; + final TextPainter painter; + @override TextPainter get selectionPainter => painter; // SelectableTextBlock hook + + Size _size = Size.zero; + @override Size get size => _size; + + @override Size layout(double width) { painter.layout(maxWidth: width); return _size = painter.size; } + @override void paint(Canvas c, Size s, double dy) { /* decorate */ painter.paint(c, Offset(0, dy)); } + @override void handleTapDown(PointerDownEvent e) {/* see BlockPainter$Paragraph */} + @override void handleTapUp(PointerUpEvent e) {/* ... */} + @override void dispose() => painter.dispose(); +} +``` + +Wire it in via the theme: + +```dart +MarkdownThemeData( + builder: (block, theme) => block is MD$Paragraph ? MyBlock(spans: block.spans, theme: theme) : null, +); +``` + +Returning `null` falls back to the default painter for that block. For +many-painter blocks (custom lists/tables), mix in `MultiPainterSelectable` instead +and expose `fragments` + `renderedText`. + +> **Offset-space rule:** if your block is selectable, `renderedText` (and each +> fragment's `textStart`) must match `markdownBlockRenderedText(block)` (see +> [selection](selection.md)), or hit-testing, highlight, and copied text will +> disagree. + +## Default block painters + +- **`$Paragraph`** โ€” one `TextPainter` from spans, painted at `(0, offset)`; + selectable + link taps; no decoration. +- **`$Heading`** โ€” like `$Paragraph`, styled by `theme.headingStyleFor(level)`. +- **`$Quote`** โ€” body styled `theme.quoteStyle ?? textStyle`; one vertical accent + bar per `indent` level (`lineIndent = 10`, `dividerColor`); text shifted right + by `lineIndent + indent*lineIndent` (also its `selectionOrigin`). +- **`$Alert`** โ€” GitHub admonition: tinted rounded background (accent ฮฑ 0.10), + left accent bar (width 4), bold colored title above body (`alert.title`, + `alertColorFor(type)`); body selectable via `selectionOrigin`. +- **`$Code`** โ€” monospace `TextPainter` (raw text, no span parsing); rounded + `surfaceColor` background (radius = padding 8); taps are no-ops; **no link taps**. +- **`$List`** โ€” recursive nested items (`_baseIndent = 8`, `_levelIndent = 16` per + depth); bullet glyph `โ˜‘`/`โ˜` for task items, else `โ€ข`/ordered marker; each item + is a bullet + content `TextPainter`; `MultiPainterSelectable`, items joined `\n`. +- **`$Table`** โ€” per-cell `TextPainter` (bold header), per-column alignment; + column widths via `_distributeWidths` (natural if they fit, else shrink toward + per-column min = longest-word width, else overflow); zebra rows, cached inner + grid + outer border; `MultiPainterSelectable`, cells joined `\t`, rows `\n`. +- **`$Divider`** โ€” one horizontal line across `size.width`; not selectable. +- **`$Spacer`** โ€” blank vertical gap `Size(0, fontSize * count)`; `paint` is a + no-op; not selectable. + +`$Divider`/`$Spacer` are not selectable; `$Code` is selectable but has no link +taps; only `$List`/`$Table` use `MultiPainterSelectable`. + +## Theme customization (`MarkdownThemeData`) + +`MarkdownThemeData implements ThemeExtension` โ€” put it in +`ThemeData.extensions` (it `lerp`s; non-lerpable fields switch at `t < 0.5`) or +provide it through the `MarkdownTheme` inherited widget (`MarkdownTheme.of/maybeOf`). +`MarkdownThemeData.mergeTheme(ThemeData, ...)` derives one from a Material theme. + +Render-override hooks: + +- **`builder`** `BlockPainter? Function(MD$Block, MarkdownThemeData)` โ€” custom + painter per block; `null` โ‡’ default. +- **`blockFilter`** `bool Function(MD$Block)` โ€” drop a whole block before painters + are built; `_sourceIndices` keeps selection anchored to the real model index. +- **`spanFilter`** `bool Function(MD$Span)` โ€” filter inline spans. **Caveat: + dropping text-bearing spans shifts the painter's offset space vs the model, so + the highlight stays right but copied text can misalign. Avoid dropping + text-bearing spans when selection is enabled.** + +Styling: `textStyle`, per-level `h1Style..h6Style` (+ cached `headingStyleFor`), +`textStyleFor(MD$Style)` (cached mapping of the bitmask โ†’ bold/italic/underline/ +strikethrough/monospace + highlight/monospace backgrounds + link color), +`linkColor`/`linkStyle`, `surfaceColor` (code/table/quote backgrounds), +`highlightBackgroundColor`, `monospaceBackgroundColor`, `dividerColor`, +`alertColors` (+ built-in GitHub palette fallback via `alertColorFor`), +`textDirection`, `textScaler`, and `onLinkTap`. + +## Public vs internal + +Public (in `flutter_md.dart`'s `show` list): the framework +(`BlockPainter`, `SelectableBlockPainter`, `SelectableTextBlock`, +`MultiPainterSelectable`, `SelectableFragment`, `ParagraphGestureHandler`, +`paragraphFromMarkdownSpans`) and the nine defaults (`BlockPainter$Paragraph โ€ฆ +$Spacer`). Internal (`@meta.internal`, only via `src/render.dart`): +`MarkdownPainter`, `MarkdownRenderObject`. + +## Invariants (repeated from [architecture](architecture.md), owned here) + +- Glyphs cached in a `ui.Picture` keyed by size; reused on repaint; nulled only on + `update`/`invalidateLayout`. +- **Selection highlight is painted outside that cache, on top of the glyphs** + (`MarkdownRenderObject.paint` calls `_painter.paint(...)` then + `_painter.paintHighlight(...)`), so drags/streaming never rebuild the glyph + cache, and a translucent highlight stays visible over opaque block/inline + backgrounds (code fences, `inline code`, `==mark==`). Color = + `controller.selectionColor ?? _kSelectionColor` (`0x552196F3`). +- `isRepaintBoundary => controller != null`; `alwaysNeedsCompositing => false`. +- Handle `LeaderLayer`s are pushed in `paint` (only when a scope supplied + start/end `LayerLink` + local offset) so native handles follow scrolling content. diff --git a/docs/selection.md b/docs/selection.md new file mode 100644 index 0000000..fb9e821 --- /dev/null +++ b/docs/selection.md @@ -0,0 +1,203 @@ +# Text selection + +Files: `lib/src/selection.dart`, `lib/src/selection_scope.dart`, +`lib/src/widget.dart`, and the render side in +`lib/src/render/markdown_render_object.dart`. Selection spans **across blocks and +across multiple `MarkdownWidget`s** (a whole chat), including messages whose +widgets are scrolled off and disposed (issue #25). + +## Controller-anchored architecture + +`MarkdownSelectionController extends ChangeNotifier` is the single source of +truth. State is held as **logical anchors over immutable models**, never over +render objects. + +- **`MarkdownPosition`** `{Object documentId, int blockIndex, int offset}` โ€” + `documentId` is app-defined (e.g. a chat message id); `blockIndex` indexes + `Markdown.blocks` (the source list, not painter fragments); `offset` indexes the + block's **rendered text** (ยง linearization). +- **`MarkdownSelection`** `{MarkdownPosition base, extent}` (+ `.collapsed(at)`, + `isCollapsed`). Direction is stored as authored; reading order is resolved + lazily by the controller (`_ordered`/`_compare`), never normalized. + +**Why anchor to the model:** text is derived from an app-supplied registry of +immutable `Markdown` models, so `getText()`/`selectedContent()` work even when a +widget is unmounted. A selection may span documents whose widgets a `ListView` has +disposed; only mounted docs contribute _geometry_, while _all_ in-range docs +contribute _text_. Flutter's `SelectableRegion`/custom delegates were rejected in +spikes (they glue without separators and drop disposed items). + +## Document registry + +The app registers each document's immutable model so text extraction is +mount-independent: + +- `setDocuments(Iterable)` โ€” replace the whole registry + (bulk/initial load); clamps a still-valid selection into the new docs, else + drops it. +- `putDocument(id, model, {order})` โ€” insert-or-update; **the streaming entry + point**. No-op early return when neither model nor order changed (streaming can + call once per token; the model check is `identical`). A model change triggers + reconciliation (ยง). +- `removeDocument(id)` โ€” removes and drops the selection if either endpoint was in + that doc. +- `documentCount` (prefer over `documents.length`, which allocates), `hasDocuments`, + `documents`. +- **`MarkdownDocumentRef`** `{Object id, Markdown model, int? order}` โ€” `order` + null โ‡’ registration order; supply e.g. the message index so **unmounted** docs + still order correctly. + +Ordering is O(1): `_sort()` sorts `_docs` by `order` then `_reindex()` rebuilds a +`Map _indexById`, so `_orderIndex(id)` is a map lookup. This matters +because `_orderIndex` runs several times per selectable block on **every highlight +repaint** (via `rangeFor`) โ€” a linear scan would scale with chat size and stall +drags. `_reindex` runs on set/order changes only, not on model-only updates. + +## Surfaces (mounted geometry bridge) + +`abstract interface class MarkdownSelectionSurface` is the controller's window +onto a live render object, **implemented by `MarkdownRenderObject`**: + +- `documentId`, `Rect globalBounds` +- `MarkdownPosition? positionForGlobal(Offset)` โ€” global point โ†’ logical position +- `List globalSelectionRects()` / `localSelectionRects()` โ€” highlight rects + (screen / content-local); used for handles, magnifier, toolbar anchor +- `setSelectionHandleLayers({startLink, startLocal, endLink, endLocal})` โ€” the + handle `LayerLink`s the surface paints so the overlay's handles follow content +- `repaintSelection()` โ€” repaint just the highlight; safe during build + +The controller keeps a `Map` keyed by +`documentId`; render objects `attachSurface`/`detachSurface` on attach/detach. +`rangeFor(documentId, blockIndex)` returns the selected `TextRange` within a block +(null if outside the selection) โ€” the per-block highlight lookup during paint. + +## Reconciliation (streaming) + +When `putDocument` replaces a model, `MarkdownReconciliationPolicy.remap(anchor, +old, new)` relocates each endpoint in the changed doc (returning null drops the +whole selection). **No stable block id exists โ€” remapping is content-based.** + +- `appendFastPath()` โ€” keep the block index verbatim when everything before the + anchor is unchanged and the anchor block's new text is a prefix-superset; else + clamp. Cheapest; correct for appends, drifts on front/mid inserts. +- **`contentAnchored()` โ€” the default.** Append fast path, else relocate by + matching the anchor block's rendered text in the new model, else clamp. Robust + to inserts/reorders without an id. +- `clearOnChange()` โ€” drop the selection on any change to the anchor's doc. + +Set via `MarkdownSelectionController(reconciliation: โ€ฆ)`. + +## Extraction & formatting + +`selectedContent()` โ†’ `MarkdownSelectedContent { List +documents; isEmpty; toPlainText() }`, built from the models in reading order, +independent of what's mounted (empty slices skipped). + +- `MarkdownSelectedDocument { documentId, List blocks }` +- `MarkdownSelectedBlock { int blockIndex, String type, String text, TextRange +renderedRange, TextRange? sourceRange /* currently always null */, MD$Block block }` + +`getText([formatter])` = `(formatter ?? controller.formatter).format(selectedContent())`. + +- `MarkdownSelectionFormatter` โ€” `String format(MarkdownSelectedContent)`. +- `MarkdownPlainTextFormatter` (default) โ€” joins block slices within a doc by + `blockSeparator` (`'\n'`), docs by `documentSeparator` (`'\n\n'`); empty blocks + skipped. Implement your own for "copy as Markdown" etc. + +### `markdownBlockRenderedText(block)` โ€” the linearization + +This is the **single source of truth** shared by extraction and pointer +hit-testing (the space `TextPainter.getPositionForOffset` indexes). A selectable +block painter's `renderedText`/fragment offsets **must** match it: + +- paragraph/heading/quote/alert โ†’ concatenated span text. **Alert = body only; + the title contributes nothing.** +- code โ†’ raw `text`. +- **list** โ†’ items depth-first, joined by `'\n'` unconditionally (one line per + item, even empty ones); checkbox glyphs contribute nothing. +- **table** โ†’ cells joined by `'\t'`, rows by `'\n'`. +- divider / spacer โ†’ `''` (structural blocks contribute no text). + +## `MarkdownSelectionScope` โ€” gestures, keyboard, handles, toolbar + +`MarkdownSelectionScope` (a `StatefulWidget`) owns interaction for its subtree and +exposes the controller to descendant `MarkdownWidget`s via an inherited widget. +`MarkdownSelectionScopeState` is **public** so a custom toolbar can drive it. + +Params: `controller` (required), `child`, `focusNode`, `enabled` (false โ‡’ inert +but still exposes the controller), `selectionColor`, `contextMenuBuilder` (null โ‡’ +no toolbar), `magnifierConfiguration`, `selectionControls`, `onSelectionChanged`. +Statics: `MarkdownSelectionScope.of/maybeOf` (โ†’ controller), `stateOf` (โ†’ state). + +- **Gestures:** a `TapAndPanGestureRecognizer` restricted to mouse/stylus/trackpad + with `DragStartBehavior.down` handles both taps and drags from one recognizer so + consecutive-tap counting stays intact โ€” **single** click collapses/clears, + **double** selects the word, **triple** selects the block, `Shift`-click extends, + and a drag after a double/triple click keeps word/block granularity. On **touch** + a `LongPressGestureRecognizer` grabs the whole word then extends by word, and a + `DoubleTapGestureRecognizer` selects the word + pops the toolbar (both tap/press + based, so a plain swipe still scrolls an enclosing `ListView`). A secondary-only + `TapGestureRecognizer` shows the toolbar on right-click; with no primary + callbacks it never competes with the tap-and-pan recognizer. Word boundaries + come from the mounted painter's `TextPainter.getWordBoundary` (via + `MarkdownSelectionSurface.wordBoundaryForGlobal`), with the text-based + `wordRangeIn` heuristic as an unmounted-document fallback. +- **Cursor:** the `MarkdownWidget` render object is a `MouseTrackerAnnotation`; it + shows the click (hand) cursor over an actionable link (a span with a tap + recognizer, detected on hover via `handleEvent` โ†’ `markNeedsPaint` so + `MouseTracker` re-reads the cursor), the text (I-beam) cursor while selectable, + and otherwise `MouseCursor.defer`. +- **Keyboard:** an `Actions` map bound to the ambient `DefaultTextEditingShortcuts` + Intents (installed by `WidgetsApp`/`MaterialApp`): Ctrl/Cmd+C copy, Ctrl/Cmd+A + select-all, Shift+arrows extend (char/word/line/doc), Esc clear. Extension + intents no-op when `collapseSelection` is true (unshifted arrows don't move it). +- **Native handles + magnifier:** touch platforms only (android/iOS/fuchsia). A + Flutter `SelectionOverlay` is driven from `controller.selectionHandleEndpoints()`; + handle drags call `controller.moveSelectionEdgeToGlobal(...)`. Overlay creation + is skipped when there's no `Overlay` host. +- **Toolbar:** `ContextMenuController` + `AdaptiveTextSelectionToolbar`; default + items are Copy (non-collapsed selection) and Select-all (`hasDocuments`). + Public ops: `copySelection`, `selectAll`, `clearSelection`, `showToolbar`, + `hideToolbar`. +- **`MarkdownSelectionGroup`:** pass the same group to several controllers and at + most one has an active selection โ€” a new non-collapsed selection clears the + others. `clearExternal()` clears all (call when a non-Markdown `SelectableText`/ + `SelectionArea` starts its own selection). + +## Opting a `MarkdownWidget` into selection + +`MarkdownWidget` is selectable only when given a `documentId` **and** a resolvable +controller (explicit `controller:` or ambient `MarkdownSelectionScope.maybeOf`); +otherwise it's inert. **The app must register the document's model** with the +controller (`setDocuments`/`putDocument`) โ€” the widget does not self-register. +Typical pattern: keep models in a list, feed them to the controller, and give each +`MarkdownWidget` its matching `documentId`. + +## Gotchas / known limitations + +- `selectionColor` setter repaints surfaces directly and must **not** + `notifyListeners` โ€” it's applied during build (`didChangeDependencies`), where + notifying would trigger `setState`. (The `formatter` setter _does_ notify.) +- Alert **title** is not selectable (body only). +- Keyboard word/line extension is **block-approximate** (a whitespace scan / jump + to block start-end within the linearized text, not visual lines). Only + `extendSelectionToAdjacentLine` uses real on-screen geometry (and no-ops when the + endpoint is unmounted). +- Handle anchors can lag after a **reflow with no selection change** (streaming to + another block, font/scale change) until the next selection change โ€” the overlay + sync is driven by selection-change notifications only. +- `moveSelectionEdgeToGlobal` refuses a drag that would collapse the selection (to + avoid disposing the overlay mid-gesture). +- `MarkdownSelectedBlock.sourceRange` is documented as best-effort but is currently + always null. +- `MarkdownThemeData.blockFilter` drops whole blocks at render time, but selection + **extraction is model-based** and does not see that filtering. A selection spanning + _across_ a dropped block therefore copies the hidden block's text even though the + block is never highlighted on screen. Avoid `blockFilter` when selection is enabled + (same guidance as `spanFilter`, which shifts the painter's offset space). +- Keyboard **word** navigation indexes the block's rendered text by UTF-16 code unit, + so it may split a surrogate pair (an emoji / other non-BMP character). Accepted v1 + limitation. +- Documents are ordered by their `order` key via `List.sort`, which is **not stable**, + so documents sharing an identical `order` have unspecified relative order. Supply + unique `order` values (e.g. the message index). diff --git a/example/lib/main.dart b/example/lib/main.dart index 6373591..ec3ef34 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_md/flutter_md.dart'; +import 'tabs/chat_tab.dart'; +import 'tabs/highlight_tab.dart'; +import 'tabs/lorem_tab.dart'; + void main() => runZonedGuarded( () => runApp(ThemeModel( notifier: ValueNotifier(ThemeMode.dark), @@ -86,7 +90,7 @@ class ThemeModel extends InheritedNotifier> { } /// {@template home_screen} -/// HomeScreen widget. +/// HomeScreen widget: a tabbed showcase of the markdown renderer. /// {@endtemplate} class HomeScreen extends StatefulWidget { /// {@macro home_screen} @@ -99,7 +103,67 @@ class HomeScreen extends StatefulWidget { } /// State for widget HomeScreen. -class _HomeScreenState extends State { +class _HomeScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabs = TabController(length: 4, vsync: this); + + @override + void dispose() { + _tabs.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Text('Markdown'), + actions: [ + Switch.adaptive( + value: ThemeModel.of(context).value == ThemeMode.dark, + onChanged: (value) { + ThemeModel.of(context).value = + value ? ThemeMode.dark : ThemeMode.light; + }, + ), + ], + bottom: TabBar( + controller: _tabs, + tabs: const [ + Tab(text: 'Editor', icon: Icon(Icons.edit)), + Tab(text: 'Selection', icon: Icon(Icons.text_fields)), + Tab(text: 'Chat', icon: Icon(Icons.chat_bubble_outline)), + Tab(text: 'Highlight', icon: Icon(Icons.code)), + ], + ), + ), + body: SafeArea( + child: TabBarView( + controller: _tabs, + children: const [ + EditorTab(), + LoremTab(), + ChatTab(), + HighlightTab(), + ], + ), + ), + ); +} + +/// {@template editor_tab} +/// A split-pane live Markdown editor (source on one side, render on the other). +/// {@endtemplate} +class EditorTab extends StatefulWidget { + /// {@macro editor_tab} + const EditorTab({super.key}); + + @override + State createState() => _EditorTabState(); +} + +/// State for widget EditorTab. +class _EditorTabState extends State { final MultiChildLayoutDelegate _layoutDelegate = _HomeScreenLayoutDelegate(); final TextEditingController _inputController = TextEditingController(text: _markdownExample); @@ -138,100 +202,84 @@ class _HomeScreenState extends State { } @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - centerTitle: true, - title: const Text('Markdown'), - actions: [ - // theme switch widget - Switch.adaptive( - value: ThemeModel.of(context).value == ThemeMode.dark, - onChanged: (value) { - ThemeModel.of(context).value = - value ? ThemeMode.dark : ThemeMode.light; - }), - ]), - body: SafeArea( - child: CustomMultiChildLayout( - delegate: _layoutDelegate, - children: [ - LayoutId( - id: 0, - child: Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Stack( - fit: StackFit.expand, - children: [ - Positioned.fill( - child: TextField( - controller: _inputController, - expands: true, - maxLines: null, - minLines: null, - keyboardType: TextInputType.multiline, - decoration: const InputDecoration( - border: InputBorder.none, - hintText: '____________________________________\n' - '______________________________\n' - '__________________________\n' - '______________________________\n' - '____________________________________\n' - '________________________\n' - '________________________________________\n' - '______________________________\n' - '________________________\n' - '__________________________________________\n' - '______________________________\n', - ), - ), + Widget build(BuildContext context) => CustomMultiChildLayout( + delegate: _layoutDelegate, + children: [ + LayoutId( + id: 0, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: TextField( + controller: _inputController, + expands: true, + maxLines: null, + minLines: null, + keyboardType: TextInputType.multiline, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: '____________________________________\n' + '______________________________\n' + '__________________________\n' + '______________________________\n' + '____________________________________\n' + '________________________\n' + '________________________________________\n' + '______________________________\n' + '________________________\n' + '__________________________________________\n' + '______________________________\n', ), - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(right: 12.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconButton.filledTonal( - icon: const Icon( - Icons.refresh, - ), - onPressed: () => - _inputController.text = _markdownExample, - ), - ], + ), + ), + Align( + alignment: Alignment.topRight, + child: Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton.filledTonal( + icon: const Icon( + Icons.refresh, + ), + onPressed: () => + _inputController.text = _markdownExample, ), - ), + ], ), - ], + ), ), - ), + ], ), ), - LayoutId( - id: 1, - child: Align( - alignment: Alignment.topLeft, - child: Card( - child: SingleChildScrollView( - primary: false, - padding: const EdgeInsets.all(8.0), - child: ValueListenableBuilder( - valueListenable: _outputController, - builder: (context, value, child) => MarkdownWidget( - markdown: value, - ), - ), + ), + ), + LayoutId( + id: 1, + child: Align( + alignment: Alignment.topLeft, + child: Card( + child: SingleChildScrollView( + primary: false, + padding: const EdgeInsets.all(8.0), + child: ValueListenableBuilder( + valueListenable: _outputController, + builder: (context, value, child) => MarkdownWidget( + markdown: value, ), ), ), ), - ], + ), ), - ), + ], ); } diff --git a/example/lib/tabs/chat_tab.dart b/example/lib/tabs/chat_tab.dart new file mode 100644 index 0000000..b7a99f1 --- /dev/null +++ b/example/lib/tabs/chat_tab.dart @@ -0,0 +1,567 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_md/flutter_md.dart'; + +/// A chat-like tab: messages in a `ListView.builder` with cross-message text +/// selection driven by a single [MarkdownSelectionController]. Because the +/// selection is anchored on the immutable models, it survives messages being +/// scrolled off-screen (and disposed), and the whole selection can be copied at +/// any time. +/// +/// The conversation is intentionally long and varied โ€” headings, tables, code, +/// nested and task lists, block quotes, GitHub alerts and inline math โ€” to +/// exercise selection across every block type. The "Stream" button appends a +/// new assistant reply and grows it token-by-token via a +/// [StreamingMarkdownParser], so completed blocks are parsed once and only the +/// live tail is re-parsed as tokens arrive โ€” while any active selection stays +/// anchored (content-based reconciliation). +class ChatTab extends StatefulWidget { + /// Creates the chat demo tab. + const ChatTab({super.key}); + + @override + State createState() => _ChatTabState(); +} + +class _ChatTabState extends State { + final MarkdownSelectionController _controller = MarkdownSelectionController(); + final ScrollController _scroll = ScrollController(); + late final List<_Msg> _messages = _seed(); + + Timer? _streamTimer; + List _streamTokens = const []; + int _streamCursor = 0; + + /// Incremental parser for the reply currently streaming in. Reused (via + /// [StreamingMarkdownParser.reset]) for each new streamed message so closed + /// blocks are never re-parsed. + final StreamingMarkdownParser _streamParser = StreamingMarkdownParser(); + + bool get _isStreaming => _streamTimer != null; + + @override + void initState() { + super.initState(); + _controller.setDocuments(_refs()); + } + + @override + void dispose() { + _streamTimer?.cancel(); + _scroll.dispose(); + _controller.dispose(); + super.dispose(); + } + + List _refs() => [ + for (final (i, m) in _messages.indexed) + MarkdownDocumentRef(id: m.id, model: m.markdown, order: i), + ]; + + Future _copy() async { + final text = _controller.getText(); + if (text.isEmpty) { + _toast('Nothing selected โ€” drag across a few messages first.'); + return; + } + await Clipboard.setData(ClipboardData(text: text)); + if (!mounted) return; + final preview = text.length > 160 ? '${text.substring(0, 160)}โ€ฆ' : text; + _toast('Copied ${text.length} characters:\n$preview'); + } + + void _toast(String message) => ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar(SnackBar(content: Text(message))); + + // --- streaming ----------------------------------------------------------- + + void _toggleStream() { + if (_isStreaming) { + _stopStream(); + return; + } + final id = 'stream-${DateTime.now().microsecondsSinceEpoch}'; + _streamTokens = _streamAnswer.split(' '); + _streamCursor = 0; + _streamParser.reset(); + setState(() => _messages.add(_Msg(id, false, const Markdown.empty()))); + _controller.putDocument(id, const Markdown.empty(), + order: _messages.length - 1); + _scrollToBottom(); + _streamTimer = + Timer.periodic(const Duration(milliseconds: 55), (_) => _tick(id)); + } + + void _tick(String id) { + if (_streamCursor >= _streamTokens.length) { + _stopStream(); + return; + } + final token = _streamTokens[_streamCursor++]; + // Feed only the newly-arrived delta; the parser reuses the already-parsed + // prefix and re-parses just the live tail โ€” no full re-parse per token. + final delta = _streamParser.source.isEmpty ? token : ' $token'; + final grown = _streamParser.add(delta); + final idx = _messages.indexWhere((m) => m.id == id); + if (idx < 0) { + _stopStream(); + return; + } + setState(() => _messages[idx] = _messages[idx].withMarkdown(grown)); + // Reconciliation keeps any active selection anchored across the update. + _controller.putDocument(id, grown, order: idx); + _stickToBottom(); + } + + void _stopStream() { + _streamTimer?.cancel(); + _streamTimer = null; + if (mounted) setState(() {}); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_scroll.hasClients) return; + _scroll.animateTo( + _scroll.position.maxScrollExtent, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + }); + } + + /// Keeps the view pinned to the bottom while streaming, but only if the user + /// hasn't scrolled up to read/select earlier messages. + void _stickToBottom() { + if (!_scroll.hasClients) return; + final pos = _scroll.position; + if (pos.maxScrollExtent - pos.pixels < 120) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scroll.hasClients) { + _scroll.jumpTo(_scroll.position.maxScrollExtent); + } + }); + } + } + + @override + Widget build(BuildContext context) => Column( + children: [ + Expanded( + child: MarkdownSelectionScope( + controller: _controller, + child: ListView.builder( + controller: _scroll, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: _messages.length, + itemBuilder: (context, i) => _Bubble(message: _messages[i]), + ), + ), + ), + _SelectionBar( + controller: _controller, + isStreaming: _isStreaming, + onCopy: _copy, + onSelectAll: _controller.selectAll, + onClear: _controller.clear, + onStream: _toggleStream, + ), + ], + ); +} + +class _SelectionBar extends StatelessWidget { + const _SelectionBar({ + required this.controller, + required this.isStreaming, + required this.onCopy, + required this.onSelectAll, + required this.onClear, + required this.onStream, + }); + + final MarkdownSelectionController controller; + final bool isStreaming; + final Future Function() onCopy; + final VoidCallback onSelectAll; + final VoidCallback onClear; + final VoidCallback onStream; + + @override + Widget build(BuildContext context) => Material( + elevation: 8, + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + children: [ + Expanded( + child: AnimatedBuilder( + animation: controller, + builder: (context, _) { + final n = controller.getText().length; + return Text( + n == 0 + ? 'Drag / long-press-drag across messages ยท ' + 'Ctrl/Cmd+C copy ยท right-click or long-press ' + 'for the toolbar ยท handles on touch' + : 'Selected $n characters across messages', + style: Theme.of(context).textTheme.bodySmall, + ); + }, + ), + ), + IconButton( + tooltip: 'Select all', + onPressed: onSelectAll, + icon: const Icon(Icons.select_all), + ), + IconButton( + tooltip: 'Clear selection', + onPressed: onClear, + icon: const Icon(Icons.clear), + ), + TextButton.icon( + onPressed: onStream, + icon: Icon(isStreaming ? Icons.stop : Icons.bolt), + label: Text(isStreaming ? 'Stop' : 'Stream'), + ), + const SizedBox(width: 4), + FilledButton.icon( + onPressed: onCopy, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ], + ), + ), + ), + ); +} + +class _Bubble extends StatelessWidget { + const _Bubble({required this.message}); + + final _Msg message; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final isUser = message.isUser; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: + isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isUser) ...[ + const _Avatar(isUser: false), + const SizedBox(width: 8), + ], + Flexible( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + constraints: BoxConstraints( + maxWidth: MediaQuery.sizeOf(context).width * 0.78), + decoration: BoxDecoration( + color: isUser + ? scheme.primaryContainer + : scheme.surfaceContainerHighest, + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(16), + topRight: const Radius.circular(16), + bottomLeft: Radius.circular(isUser ? 16 : 4), + bottomRight: Radius.circular(isUser ? 4 : 16), + ), + ), + child: message.markdown.isEmpty + ? const _TypingDots() + : MarkdownWidget( + markdown: message.markdown, documentId: message.id), + ), + ), + if (isUser) ...[ + const SizedBox(width: 8), + const _Avatar(isUser: true), + ], + ], + ), + ); + } +} + +class _Avatar extends StatelessWidget { + const _Avatar({required this.isUser}); + final bool isUser; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return CircleAvatar( + radius: 16, + backgroundColor: isUser ? scheme.primary : scheme.secondary, + foregroundColor: isUser ? scheme.onPrimary : scheme.onSecondary, + child: Icon(isUser ? Icons.person : Icons.smart_toy_outlined, size: 18), + ); + } +} + +/// A small animated "typing" indicator shown while a streamed reply is empty. +class _TypingDots extends StatefulWidget { + const _TypingDots(); + + @override + State<_TypingDots> createState() => _TypingDotsState(); +} + +class _TypingDotsState extends State<_TypingDots> + with SingleTickerProviderStateMixin { + late final AnimationController _c = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + )..repeat(); + + @override + void dispose() { + _c.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).colorScheme.onSurfaceVariant; + return SizedBox( + width: 40, + height: 16, + child: AnimatedBuilder( + animation: _c, + builder: (context, _) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < 3; i++) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Opacity( + opacity: 0.3 + 0.7 * _phase(i), + child: CircleAvatar(radius: 3, backgroundColor: color), + ), + ), + ], + ), + ), + ); + } + + double _phase(int i) { + final t = (_c.value + i / 3) % 1.0; + return t < 0.5 ? t * 2 : (1 - t) * 2; + } +} + +class _Msg { + const _Msg(this.id, this.isUser, this.markdown); + final String id; + final bool isUser; + final Markdown markdown; + _Msg withMarkdown(Markdown m) => _Msg(id, isUser, m); +} + +/// The answer streamed in token-by-token when the "Stream" button is pressed. +/// +/// It is deliberately multi-block (blank-line separated) so the +/// [StreamingMarkdownParser] can *freeze* each completed block: once a blank +/// line proves a paragraph or list is done, it is never re-parsed again โ€” only +/// the final, still-growing block is. +const String _streamAnswer = 'Absolutely โ€” here is a streamed reply.\n' + '\n' + 'Because the selection is anchored on the **immutable model**, it stays ' + 'put while these words arrive one at a time.\n' + '\n' + 'And this reply is parsed **incrementally**:\n' + '\n' + '- completed blocks are parsed once, then frozen\n' + '- only the live tail re-parses on each token\n' + '- so long messages stay cheap to grow\n' + '\n' + 'Try selecting an earlier message first, then press Stream and watch the ' + 'highlight hold while these blocks stream in.'; + +List<_Msg> _seed() { + final data = <(bool, String)>[ + ( + true, + 'Hi! Can you give me a **quick tour** of what this Markdown renderer ' + 'can display?', + ), + ( + false, + ''' +## Welcome ๐Ÿ‘‹ + +`flutter_md` renders GitHub-Flavored Markdown on a **cached canvas** โ€” no +per-glyph widgets โ€” so long chats stay smooth. It supports: + +- Headings, paragraphs, **bold**, _italic_, `inline code`, ~~strikethrough~~ +- Ordered, unordered, **nested** and task lists +- Tables, block quotes, fenced code, thematic breaks +- GitHub alerts and opt-in inline math + +> [!NOTE] +> Every block below is selectable โ€” drag right across the bubbles.''', + ), + (true, 'Nice. Show me a **table** comparing the block types.'), + ( + false, + ''' +Here you go: + +| Block | Example | Selectable | +| ------------ | -------------------- | :--------: | +| Heading | `# Title` | โœ… | +| Paragraph | plain text | โœ… | +| List | `- item` | โœ… | +| Table | this one | โœ… | +| Code | `fenced` | โœ… | +| Quote | `> quote` | โœ… | + +Try dragging from the header row down to the last cell.''', + ), + (true, 'How fast is it? Any `benchmarks`?'), + ( + false, + ''' +The parser got roughly **45% faster** in `0.1.0`. Rough figures on a laptop: + +| Scenario | Before | After | +| ------------------- | -----: | ----: | +| Parse 50-block doc | 1.8 ms | 1.0 ms | +| Paint (cache hit) | 41 ยตs | 41 ยตs | +| Paint (cache miss) | 6.5 ms | 6.5 ms | + +> Headless timings are relative โ€” real FPS is confirmed with +> `flutter run --profile` and DevTools.''', + ), + (true, 'Show me how to **embed** it.'), + ( + false, + ''' +Three steps: + +1. Add the dependency to `pubspec.yaml`. +2. Parse once, cache the result. +3. Hand the model to `MarkdownWidget`. + +```dart +final md = Markdown.fromString('# Hello'); + +@override +Widget build(BuildContext context) => MarkdownWidget( + markdown: md, + documentId: 'msg-1', // opt into selection + ); +``` + +> [!TIP] +> Parse off the build method and reuse the `Markdown` instance โ€” it is +> immutable and cheap to hold.''', + ), + (true, 'What about **math**? I write a lot of formulas.'), + ( + false, + r''' +Inline math is opt-in (`inlineMath: true`). Then `$...$` renders: +$\alpha$, $\beta$, $\pi \approx 3.14$, $x \rightarrow \infty$, with +superscripts and subscripts like $x^2$ and $H_2O$. + +The quadratic roots are $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.''', + ), + (true, 'Do **nested** lists and **task** lists work?'), + ( + false, + ''' +Both, to arbitrary depth: + +- Parsing + - Tokenize + - Build the block tree + - Inline spans + - Source offsets +- Rendering + - Layout + - Paint + +And a checklist: + +- [x] Parser +- [x] GitHub alerts +- [x] Text selection + - [x] Cross-block + - [x] Cross-widget (this chat) +- [ ] Native mobile handles''', + ), + (true, 'Any **warnings** I should know about?'), + ( + false, + ''' +> [!WARNING] +> Selection offsets are anchored on the *rendered* text, not the raw source โ€” +> markup characters are not part of the offset space. + +> [!CAUTION] +> Dropping text-bearing spans with a `spanFilter` shifts that offset space, so +> the highlight stays correct but copied text may drift. Avoid it when +> selection is on.''', + ), + (true, 'Give me a bigger table โ€” say, common **HTTP status codes**.'), + ( + false, + ''' +| Code | Meaning | Category | +| ---: | --------------------- | ------------ | +| 200 | OK | Success | +| 201 | Created | Success | +| 301 | Moved Permanently | Redirect | +| 400 | Bad Request | Client error | +| 401 | Unauthorized | Client error | +| 404 | Not Found | Client error | +| 418 | I'm a teapot | Client error | +| 500 | Internal Server Error | Server error | +| 503 | Service Unavailable | Server error |''', + ), + ( + true, + 'Great. And I can **select across all of this** โ€” even the messages ' + "I've scrolled past?", + ), + ( + false, + ''' +Exactly. The selection lives in the controller as logical anchors +`(documentId, blockIndex, offset)` over the immutable models, so: + +1. It **survives disposal** when a bubble scrolls out of the lazy list. +2. `Copy` always returns the *full* text, including off-screen messages. +3. Streaming updates **reconcile** โ€” the anchor holds as text grows. + +> Scroll to the top, start a drag, scroll back down, and finish it โ€” then hit +> **Copy**. Press **Stream** to watch reconciliation in action.''', + ), + (true, 'Perfect, thanks! ๐Ÿ™'), + ( + false, + 'Anytime. Pro tip: **Select all** grabs the entire transcript at once, ' + 'and **Clear** resets it. Happy hacking with `flutter_md`!', + ), + ]; + + return <_Msg>[ + for (final (i, (isUser, text)) in data.indexed) + _Msg('m$i', isUser, Markdown.fromString(text, inlineMath: true)), + ]; +} diff --git a/example/lib/tabs/highlight_tab.dart b/example/lib/tabs/highlight_tab.dart new file mode 100644 index 0000000..bf5fa0d --- /dev/null +++ b/example/lib/tabs/highlight_tab.dart @@ -0,0 +1,257 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_md/highlight.dart'; +import 'package:flutter_md/highlight/all.dart'; +import 'package:flutter_md/highlight/themes.dart'; + +/// Showcases syntax highlighting across many popular languages. The whole +/// registry ([allHighlightLanguages]) is used here on purpose โ€” a real app +/// would list only the languages it needs so the rest tree-shakes away. +class HighlightTab extends StatefulWidget { + /// Creates the highlighting showcase tab. + const HighlightTab({super.key}); + + @override + State createState() => _HighlightTabState(); +} + +class _HighlightTabState extends State { + // Built once; the whole registry is shared between both theme variants. + final SyntaxHighlighter _dark = MarkdownHighlighter( + languages: allHighlightLanguages, + theme: HighlightThemes.githubDark, + ); + final SyntaxHighlighter _light = MarkdownHighlighter( + languages: allHighlightLanguages, + theme: HighlightThemes.githubLight, + ); + + final Markdown _doc = Markdown.fromString(_showcase); + + final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final theme = MarkdownThemeData.mergeTheme( + Theme.of(context), + highlighter: isDark ? _dark : _light, + spanFilter: (span) => !span.style.contains(MD$Style.image), + ); + return Scrollbar( + controller: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + child: MarkdownWidget(markdown: _doc, theme: theme), + ), + ); + } +} + +const String _showcase = r''' +# Syntax highlighting + +Fenced code blocks are tokenized and colored with the GitHub theme (follow the +light/dark switch above). Selection and copy stay aligned with the source โ€” the +highlighter only *partitions* text into spans, it never edits it. + +## Dart + +```dart +import 'dart:math' as math; + +Future roll(int sides) async { + final r = math.Random(); + return r.nextInt(sides) + 1; // 1..sides +} +``` + +## Python + +```python +from dataclasses import dataclass + +@dataclass +class Point: + x: float = 0.0 + y: float = 0.0 + + def dist(self) -> float: + return (self.x ** 2 + self.y ** 2) ** 0.5 # hypot +``` + +## JavaScript + +```js +const memo = new Map(); +export const fib = (n) => + n < 2 ? n : (memo.get(n) ?? memo.set(n, fib(n - 1) + fib(n - 2)).get(n)); +``` + +## TypeScript + +```typescript +interface User { id: number; name: string; } + +async function load(url: string): Promise { + const res = await fetch(`/api/${url}`); + return (await res.json()) as T; +} +``` + +## Rust + +```rust +fn main() { + let nums = vec![1, 2, 3, 4]; + let sum: i32 = nums.iter().filter(|&&x| x % 2 == 0).sum(); + println!("even sum = {sum}"); +} +``` + +## Go + +```go +package main + +import "fmt" + +func main() { + ch := make(chan int, 3) + go func() { ch <- 42 }() + fmt.Println(<-ch) +} +``` + +## Java + +```java +record Point(int x, int y) { + static Point origin() { return new Point(0, 0); } +} +``` + +## Kotlin + +```kotlin +fun main() { + val squares = (1..5).map { it * it } + println(squares.joinToString(prefix = "[", postfix = "]")) +} +``` + +## Swift + +```swift +let names = ["Ada", "Alan", "Grace"] +let greeting = names.map { "Hello, \($0)!" }.joined(separator: "\n") +print(greeting) +``` + +## C++ + +```cpp +#include +#include + +int sum(const std::vector& v) { + return std::accumulate(v.begin(), v.end(), 0); +} +``` + +## C# + +```csharp +public record Money(decimal Amount, string Currency) { + public override string ToString() => $"{Amount:F2} {Currency}"; +} +``` + +## Ruby + +```ruby +def fizzbuzz(n) + (1..n).map { |i| i % 15 == 0 ? "FizzBuzz" : i.to_s } +end +``` + +## PHP + +```php + + + +

Hello & welcome

+ + +``` + +## CSS + +```css +:root { --accent: #0969da; } +.button:hover { + color: var(--accent); + transition: color 120ms ease-in-out; +} +``` + +## SQL + +```sql +SELECT u.name, COUNT(o.id) AS orders +FROM users u +LEFT JOIN orders o ON o.user_id = u.id +WHERE u.active = TRUE +GROUP BY u.name; +``` + +## YAML + +```yaml +service: + name: web + ports: [80, 443] + env: + DEBUG: false # production +``` + +## JSON + +```json +{ + "name": "flutter_md", + "version": "0.2.0", + "keywords": ["markdown", "rendering"], + "stable": true +} +``` + +## Bash + +```bash +#!/usr/bin/env bash +set -euo pipefail +for f in *.log; do + echo "rotating ${f}" + mv -- "$f" "${f}.$(date +%s)" +done +``` +'''; diff --git a/example/lib/tabs/lorem_tab.dart b/example/lib/tabs/lorem_tab.dart new file mode 100644 index 0000000..ab034de --- /dev/null +++ b/example/lib/tabs/lorem_tab.dart @@ -0,0 +1,346 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_md/flutter_md.dart'; + +const String _loremMdA = ''' +# Cross-block selection + +**Lorem ipsum** dolor sit amet, consectetur _adipiscing_ elit. Drag from this +heading straight down through every block below โ€” headings, quotes, lists, +tables and code all join into one selection with sensible separators. + +> Ut enim ad minim veniam, quis nostrud exercitation `ullamco` laboris nisi ut +> aliquip ex ea commodo consequat. + +A nested list: + +- alpha item + - alpha one + - alpha two +- beta item +- gamma item + +And a table โ€” its cells are selectable too: + +| Lang | Typing | Year | +| ------ | -------- | ---: | +| Dart | static | 2011 | +| Python | dynamic | 1991 | +| Rust | static | 2010 | + +```dart +void main() => print('selectable code block'); +``` + +> [!TIP] +> Selecting Markdown clears the plain `SelectableText` below, and vice-versa.'''; + +const String _loremMdB = ''' +## A different document + +Duis aute irure dolor in `reprehenderit` in voluptate velit esse cillum dolore +eu fugiat nulla pariatur โ€” this block belongs to a **second** controller, so +selecting here clears the selection above. + +| Column A | Column B | +| -------- | -------- | +| one | two | +| three | four |'''; + +const String _loremPlain = + 'This is a plain SelectableText (not Markdown). Selecting here clears the ' + 'Markdown selections above โ€” and selecting Markdown clears this one.'; + +/// Reconstructs Markdown structure (heading levels, nested list markers, task +/// checkboxes, blockquotes, fenced code, pipe tables) on copy, instead of the +/// flattened plain text the default [MarkdownPlainTextFormatter] produces. +const MarkdownMarkupFormatter _markupFormatter = MarkdownMarkupFormatter(); + +/// Demonstrates cross-block selection within a single [MarkdownWidget], several +/// independent controllers that reset one another via a shared +/// [MarkdownSelectionGroup], and coordination with a plain `SelectableText`. +class LoremTab extends StatefulWidget { + /// Creates the lorem-ipsum selection demo tab. + const LoremTab({super.key}); + + @override + State createState() => _LoremTabState(); +} + +class _LoremTabState extends State { + final MarkdownSelectionGroup _group = MarkdownSelectionGroup(); + late final MarkdownSelectionController _a = + MarkdownSelectionController(group: _group); + late final MarkdownSelectionController _b = + MarkdownSelectionController(group: _group); + final Markdown _docA = Markdown.fromString(_loremMdA); + final Markdown _docB = Markdown.fromString(_loremMdB); + + // Bumping this key recreates the SelectionArea, clearing its selection when a + // Markdown selection starts (the Markdown -> plain direction of the reset). + int _plainEpoch = 0; + bool _mdActive = false; + + @override + void initState() { + super.initState(); + _a.setDocuments( + [MarkdownDocumentRef(id: 'A', model: _docA)]); + _b.setDocuments( + [MarkdownDocumentRef(id: 'B', model: _docB)]); + _a.addListener(_onMarkdownSelection); + _b.addListener(_onMarkdownSelection); + } + + bool _isActive(MarkdownSelectionController c) => + c.selection != null && !c.selection!.isCollapsed; + + void _onMarkdownSelection() { + final active = _isActive(_a) || _isActive(_b); + setState(() { + if (active && !_mdActive) _plainEpoch++; // clear the plain SelectableText + _mdActive = active; + }); + } + + @override + void dispose() { + _a + ..removeListener(_onMarkdownSelection) + ..dispose(); + _b + ..removeListener(_onMarkdownSelection) + ..dispose(); + super.dispose(); + } + + MarkdownSelectionController? get _activeController => + _isActive(_a) ? _a : (_isActive(_b) ? _b : null); + + Future _copy({MarkdownSelectionFormatter? formatter}) async { + final text = _activeController?.getText(formatter) ?? ''; + if (text.isEmpty) return; + await Clipboard.setData(ClipboardData(text: text)); + if (!mounted) return; + final how = formatter == null ? 'plain text' : 'Markdown'; + ScaffoldMessenger.of(context) + ..clearSnackBars() + ..showSnackBar(SnackBar( + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + content: Text('Copied ${text.length} chars as $how'), + )); + } + + Widget _label(String text) => Padding( + padding: const EdgeInsets.only(bottom: 8, top: 4), + child: Text(text, style: Theme.of(context).textTheme.labelLarge), + ); + + /// A custom [contextMenuBuilder] that appends "Copy as Markdown" and + /// "Copy LOUD" actions to the default Copy / Select-all buttons. + Widget _loudContextMenu( + BuildContext context, + MarkdownSelectionScopeState state, + ) => + AdaptiveTextSelectionToolbar.buttonItems( + anchors: state.contextMenuAnchors, + buttonItems: [ + ...state.contextMenuButtonItems, + ContextMenuButtonItem( + label: 'Copy as Markdown', + onPressed: () { + // Reconstruct structure (headings, nested lists, tables, โ€ฆ) + // instead of the flattened plain text the default Copy uses. + Clipboard.setData(ClipboardData( + text: state.controller.getText(_markupFormatter))); + state.hideToolbar(); + }, + ), + ContextMenuButtonItem( + label: 'Copy LOUD', + onPressed: () { + Clipboard.setData(ClipboardData( + text: state.controller.getText().toUpperCase())); + state.hideToolbar(); + }, + ), + ], + ); + + /// Keyboard/gesture hint shown while nothing is selected. + Widget _hint() => Text( + 'Drag to select ยท Ctrl/Cmd+A all ยท Shift+arrows extend ยท ' + 'right-click for the toolbar ยท Esc clears', + style: Theme.of(context).textTheme.bodySmall, + ); + + /// A live, side-by-side preview of what the two Copy buttons produce for the + /// current selection โ€” the whole point of [MarkdownMarkupFormatter] is that + /// the right column keeps the structure the left column flattens away. + Widget _preview() { + final c = _activeController; + final plain = c?.getText() ?? ''; + final markdown = c?.getText(_markupFormatter) ?? ''; + return LayoutBuilder( + builder: (context, constraints) { + final plainPanel = + _previewPanel('Plain (default)', plain, accent: false); + final mdPanel = + _previewPanel('Copy as Markdown', markdown, accent: true); + if (constraints.maxWidth > 620) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: plainPanel), + const SizedBox(width: 10), + Expanded(child: mdPanel), + ], + ); + } + return Column( + children: [ + plainPanel, + const SizedBox(height: 8), + mdPanel, + ], + ); + }, + ); + } + + Widget _previewPanel(String title, String body, {required bool accent}) { + final scheme = Theme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: accent + ? scheme.primaryContainer.withValues(alpha: 0.35) + : scheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: accent + ? scheme.primary.withValues(alpha: 0.5) + : scheme.outlineVariant, + ), + ), + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.bold, + color: accent ? scheme.primary : scheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 108), + child: SingleChildScrollView( + child: Text( + body.isEmpty ? 'โ€”' : body, + style: const TextStyle( + fontFamily: 'monospace', + fontFamilyFallback: ['Courier'], + fontSize: 12, + height: 1.35, + ), + ), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) => Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _label('Markdown A โ€” custom toolbar (right-click / ' + 'long-press for "Copy as Markdown" & "Copy LOUD")'), + MarkdownSelectionScope( + controller: _a, + contextMenuBuilder: _loudContextMenu, + child: MarkdownWidget(markdown: _docA, documentId: 'A'), + ), + const Divider(height: 40), + _label('Markdown B โ€” custom selection color ' + '(selecting one clears the other)'), + MarkdownSelectionScope( + controller: _b, + selectionColor: Colors.amber.withValues(alpha: 0.4), + child: MarkdownWidget(markdown: _docB, documentId: 'B'), + ), + const Divider(height: 40), + _label( + 'Plain SelectableText โ€” resets with the Markdown ones'), + SelectionArea( + key: ValueKey(_plainEpoch), + onSelectionChanged: (content) { + if ((content?.plainText ?? '').isNotEmpty) { + _group.clearExternal(); + } + }, + child: const Text(_loremPlain, + style: TextStyle(fontSize: 16, height: 1.4)), + ), + ], + ), + ), + ), + Material( + elevation: 8, + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + _mdActive + ? 'Live preview โ€” "Copy as Markdown" keeps the ' + 'structure that plain copy flattens:' + : 'Select any Markdown above to preview & copy it ' + 'two ways:', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 10), + _mdActive ? _preview() : _hint(), + const SizedBox(height: 10), + Wrap( + alignment: WrapAlignment.end, + spacing: 8, + runSpacing: 8, + children: [ + OutlinedButton.icon( + onPressed: _mdActive + ? () => _copy(formatter: _markupFormatter) + : null, + icon: const Icon(Icons.data_object), + label: const Text('Copy as Markdown'), + ), + FilledButton.icon( + onPressed: _mdActive ? _copy : null, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); +} diff --git a/example/test/smoke_test.dart b/example/test/smoke_test.dart new file mode 100644 index 0000000..31d82c2 --- /dev/null +++ b/example/test/smoke_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:md_example/main.dart'; +import 'package:md_example/tabs/highlight_tab.dart'; + +void main() { + testWidgets('all tabs build and selection drags do not crash', + (tester) async { + await tester.pumpWidget(ThemeModel( + notifier: ValueNotifier(ThemeMode.light), + child: const App(), + )); + await tester.pumpAndSettle(); + + // Editor tab renders the preview. + expect(find.byType(EditorTab), findsOneWidget); + expect(tester.takeException(), isNull); + + // Selection tab: drag across the first Markdown widget. + await tester.tap(find.text('Selection')); + await tester.pumpAndSettle(); + final md = find.byType(MarkdownWidget).first; + final g = await tester.startGesture( + tester.getTopLeft(md) + const Offset(2, 4), + kind: PointerDeviceKind.mouse); + await tester.pump(const Duration(milliseconds: 150)); + await g.moveTo(tester.getCenter(md)); + await tester.pump(const Duration(milliseconds: 150)); + await g.up(); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull); + + // The live Plain-vs-Markdown preview appears once something is selected. + expect(find.text('Plain (default)'), findsOneWidget); + // "Copy as Markdown" shows as both the preview panel title and the button. + expect(find.text('Copy as Markdown'), findsWidgets); + + // Chat tab builds and the Copy button is present. + await tester.tap(find.text('Chat')); + await tester.pumpAndSettle(); + expect(find.text('Copy'), findsWidgets); + expect(tester.takeException(), isNull); + + // Highlight tab: every showcased language renders without crashing. + await tester.tap(find.text('Highlight')); + await tester.pumpAndSettle(); + expect(find.byType(HighlightTab), findsOneWidget); + expect(find.byType(MarkdownWidget), findsOneWidget); + expect(tester.takeException(), isNull); + }); +} diff --git a/lib/flutter_md.dart b/lib/flutter_md.dart index 4d6dfbb..a301310 100644 --- a/lib/flutter_md.dart +++ b/lib/flutter_md.dart @@ -3,6 +3,28 @@ library; export 'src/markdown.dart'; export 'src/nodes.dart'; export 'src/parser.dart'; -export 'src/render.dart' show BlockPainter; +export 'src/render.dart' + show + // Block-painter framework โ€” implement/extend to customize rendering. + BlockPainter, + SelectableBlockPainter, + SelectableTextBlock, + MultiPainterSelectable, + SelectableFragment, + ParagraphGestureHandler, + paragraphFromMarkdownSpans, + // Default block painters โ€” reuse, wrap or subclass them. + BlockPainter$Paragraph, + BlockPainter$Heading, + BlockPainter$Quote, + BlockPainter$Alert, + BlockPainter$Code, + BlockPainter$List, + BlockPainter$Table, + BlockPainter$Divider, + BlockPainter$Spacer; +export 'src/highlight/engine.dart' show CodeHighlightTheme, SyntaxHighlighter; +export 'src/selection.dart'; +export 'src/selection_scope.dart'; export 'src/theme.dart'; export 'src/widget.dart'; diff --git a/lib/highlight.dart b/lib/highlight.dart new file mode 100644 index 0000000..080558c --- /dev/null +++ b/lib/highlight.dart @@ -0,0 +1,22 @@ +/// Tree-shakeable syntax highlighting for fenced code blocks. +/// +/// Import this entrypoint for the engine and the [MarkdownHighlighter], then +/// one small library per language you use (e.g. `highlight/dart.dart`). +/// Languages you never import are dropped by the compiler. +/// +/// ```dart +/// import 'package:flutter_md/highlight.dart'; +/// import 'package:flutter_md/highlight/dart.dart'; +/// import 'package:flutter_md/highlight/themes.dart'; +/// +/// final theme = MarkdownThemeData( +/// textStyle: const TextStyle(), +/// highlighter: MarkdownHighlighter( +/// languages: {'dart': HighlightDart.grammar}, +/// theme: HighlightThemes.githubDark, +/// ), +/// ); +/// ``` +library; + +export 'src/highlight/engine.dart'; diff --git a/lib/highlight/all.dart b/lib/highlight/all.dart new file mode 100644 index 0000000..48a478a --- /dev/null +++ b/lib/highlight/all.dart @@ -0,0 +1,239 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'apacheconf.dart'; +import 'bash.dart'; +import 'batch.dart'; +import 'c.dart'; +import 'clike.dart'; +import 'clojure.dart'; +import 'coffeescript.dart'; +import 'cpp.dart'; +import 'csharp.dart'; +import 'css.dart'; +import 'dart.dart'; +import 'diff.dart'; +import 'docker.dart'; +import 'elixir.dart'; +import 'elm.dart'; +import 'erlang.dart'; +import 'fsharp.dart'; +import 'git.dart'; +import 'go.dart'; +import 'graphql.dart'; +import 'groovy.dart'; +import 'handlebars.dart'; +import 'haskell.dart'; +import 'html.dart'; +import 'http.dart'; +import 'ini.dart'; +import 'java.dart'; +import 'js.dart'; +import 'json.dart'; +import 'json5.dart'; +import 'jsx.dart'; +import 'julia.dart'; +import 'kotlin.dart'; +import 'latex.dart'; +import 'less.dart'; +import 'lua.dart'; +import 'makefile.dart'; +import 'markdown.dart'; +import 'markup_templating.dart'; +import 'nginx.dart'; +import 'objectivec.dart'; +import 'ocaml.dart'; +import 'perl.dart'; +import 'php.dart'; +import 'plain.dart'; +import 'powershell.dart'; +import 'protobuf.dart'; +import 'python.dart'; +import 'r.dart'; +import 'regex.dart'; +import 'ruby.dart'; +import 'rust.dart'; +import 'sass.dart'; +import 'scala.dart'; +import 'scss.dart'; +import 'solidity.dart'; +import 'sql.dart'; +import 'swift.dart'; +import 'toml.dart'; +import 'tsx.dart'; +import 'typescript.dart'; +import 'vim.dart'; +import 'wasm.dart'; +import 'xml.dart'; +import 'yaml.dart'; +export 'apacheconf.dart'; +export 'bash.dart'; +export 'batch.dart'; +export 'c.dart'; +export 'clike.dart'; +export 'clojure.dart'; +export 'coffeescript.dart'; +export 'cpp.dart'; +export 'csharp.dart'; +export 'css.dart'; +export 'dart.dart'; +export 'diff.dart'; +export 'docker.dart'; +export 'elixir.dart'; +export 'elm.dart'; +export 'erlang.dart'; +export 'fsharp.dart'; +export 'git.dart'; +export 'go.dart'; +export 'graphql.dart'; +export 'groovy.dart'; +export 'handlebars.dart'; +export 'haskell.dart'; +export 'html.dart'; +export 'http.dart'; +export 'ini.dart'; +export 'java.dart'; +export 'js.dart'; +export 'json.dart'; +export 'json5.dart'; +export 'jsx.dart'; +export 'julia.dart'; +export 'kotlin.dart'; +export 'latex.dart'; +export 'less.dart'; +export 'lua.dart'; +export 'makefile.dart'; +export 'markdown.dart'; +export 'markup_templating.dart'; +export 'nginx.dart'; +export 'objectivec.dart'; +export 'ocaml.dart'; +export 'perl.dart'; +export 'php.dart'; +export 'plain.dart'; +export 'powershell.dart'; +export 'protobuf.dart'; +export 'python.dart'; +export 'r.dart'; +export 'regex.dart'; +export 'ruby.dart'; +export 'rust.dart'; +export 'sass.dart'; +export 'scala.dart'; +export 'scss.dart'; +export 'solidity.dart'; +export 'sql.dart'; +export 'swift.dart'; +export 'toml.dart'; +export 'tsx.dart'; +export 'typescript.dart'; +export 'vim.dart'; +export 'wasm.dart'; +export 'xml.dart'; +export 'yaml.dart'; + +/// Every bundled syntax grammar, keyed by language tag and common aliases. +/// +/// Referencing this map pulls in ALL grammars, so unused languages can no longer +/// be removed by tree-shaking โ€” use it for demos or tooling. Production code +/// should assemble a map with only the languages it needs. +final Map allHighlightLanguages = { + 'apacheconf': HighlightApacheconf.grammar, + 'bash': HighlightBash.grammar, + 'batch': HighlightBatch.grammar, + 'c': HighlightC.grammar, + 'clike': HighlightClike.grammar, + 'clojure': HighlightClojure.grammar, + 'coffeescript': HighlightCoffeescript.grammar, + 'cpp': HighlightCpp.grammar, + 'csharp': HighlightCsharp.grammar, + 'css': HighlightCss.grammar, + 'dart': HighlightDart.grammar, + 'diff': HighlightDiff.grammar, + 'docker': HighlightDocker.grammar, + 'elixir': HighlightElixir.grammar, + 'elm': HighlightElm.grammar, + 'erlang': HighlightErlang.grammar, + 'fsharp': HighlightFsharp.grammar, + 'git': HighlightGit.grammar, + 'go': HighlightGo.grammar, + 'graphql': HighlightGraphql.grammar, + 'groovy': HighlightGroovy.grammar, + 'handlebars': HighlightHandlebars.grammar, + 'haskell': HighlightHaskell.grammar, + 'html': HighlightHtml.grammar, + 'http': HighlightHttp.grammar, + 'ini': HighlightIni.grammar, + 'java': HighlightJava.grammar, + 'js': HighlightJs.grammar, + 'json': HighlightJson.grammar, + 'json5': HighlightJson5.grammar, + 'jsx': HighlightJsx.grammar, + 'julia': HighlightJulia.grammar, + 'kotlin': HighlightKotlin.grammar, + 'latex': HighlightLatex.grammar, + 'less': HighlightLess.grammar, + 'lua': HighlightLua.grammar, + 'makefile': HighlightMakefile.grammar, + 'markdown': HighlightMarkdown.grammar, + 'markup-templating': HighlightMarkupTemplating.grammar, + 'nginx': HighlightNginx.grammar, + 'objectivec': HighlightObjectivec.grammar, + 'ocaml': HighlightOcaml.grammar, + 'perl': HighlightPerl.grammar, + 'php': HighlightPhp.grammar, + 'plain': HighlightPlain.grammar, + 'powershell': HighlightPowershell.grammar, + 'protobuf': HighlightProtobuf.grammar, + 'python': HighlightPython.grammar, + 'r': HighlightR.grammar, + 'regex': HighlightRegex.grammar, + 'ruby': HighlightRuby.grammar, + 'rust': HighlightRust.grammar, + 'sass': HighlightSass.grammar, + 'scala': HighlightScala.grammar, + 'scss': HighlightScss.grammar, + 'solidity': HighlightSolidity.grammar, + 'sql': HighlightSql.grammar, + 'swift': HighlightSwift.grammar, + 'toml': HighlightToml.grammar, + 'tsx': HighlightTsx.grammar, + 'typescript': HighlightTypescript.grammar, + 'vim': HighlightVim.grammar, + 'wasm': HighlightWasm.grammar, + 'xml': HighlightXml.grammar, + 'yaml': HighlightYaml.grammar, + 'atom': HighlightXml.grammar, + 'coffee': HighlightCoffeescript.grammar, + 'context': HighlightLatex.grammar, + 'cs': HighlightCsharp.grammar, + 'dockerfile': HighlightDocker.grammar, + 'dotnet': HighlightCsharp.grammar, + 'hbs': HighlightHandlebars.grammar, + 'hs': HighlightHaskell.grammar, + 'javascript': HighlightJs.grammar, + 'kt': HighlightKotlin.grammar, + 'kts': HighlightKotlin.grammar, + 'markup': HighlightHtml.grammar, + 'mathml': HighlightHtml.grammar, + 'md': HighlightMarkdown.grammar, + 'mustache': HighlightHandlebars.grammar, + 'objc': HighlightObjectivec.grammar, + 'plaintext': HighlightPlain.grammar, + 'py': HighlightPython.grammar, + 'rb': HighlightRuby.grammar, + 'rss': HighlightXml.grammar, + 'sh': HighlightBash.grammar, + 'shell': HighlightBash.grammar, + 'sol': HighlightSolidity.grammar, + 'ssml': HighlightXml.grammar, + 'svg': HighlightHtml.grammar, + 'tex': HighlightLatex.grammar, + 'text': HighlightPlain.grammar, + 'ts': HighlightTypescript.grammar, + 'txt': HighlightPlain.grammar, + 'webmanifest': HighlightJson.grammar, + 'yml': HighlightYaml.grammar, +}; diff --git a/lib/highlight/apacheconf.dart b/lib/highlight/apacheconf.dart new file mode 100644 index 0000000..1cd3dc1 --- /dev/null +++ b/lib/highlight/apacheconf.dart @@ -0,0 +1,69 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `apacheconf`. +/// +/// Import this library only when you need `apacheconf` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightApacheconf { + /// The grammar for `apacheconf`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("#.*")), + GrammarToken( + "directive-inline", + compileHighlightPattern( + "(^[\\t ]*)\\b(?:AcceptFilter|AcceptPathInfo|AccessFileName|Action|Add(?:Alt|AltByEncoding|AltByType|Charset|DefaultCharset|Description|Encoding|Handler|Icon|IconByEncoding|IconByType|InputFilter|Language|ModuleInfo|OutputFilter|OutputFilterByType|Type)|Alias|AliasMatch|Allow(?:CONNECT|EncodedSlashes|Methods|Override|OverrideList)?|Anonymous(?:_LogEmail|_MustGiveEmail|_NoUserID|_VerifyEmail)?|AsyncRequestWorkerFactor|Auth(?:BasicAuthoritative|BasicFake|BasicProvider|BasicUseDigestAlgorithm|DBDUserPWQuery|DBDUserRealmQuery|DBMGroupFile|DBMType|DBMUserFile|Digest(?:Algorithm|Domain|NonceLifetime|Provider|Qop|ShmemSize)|Form(?:Authoritative|Body|DisableNoStore|FakeBasicAuth|Location|LoginRequiredLocation|LoginSuccessLocation|LogoutLocation|Method|Mimetype|Password|Provider|SitePassphrase|Size|Username)|GroupFile|LDAP(?:AuthorizePrefix|BindAuthoritative|BindDN|BindPassword|CharsetConfig|CompareAsUser|CompareDNOnServer|DereferenceAliases|GroupAttribute|GroupAttributeIsDN|InitialBindAsUser|InitialBindPattern|MaxSubGroupDepth|RemoteUserAttribute|RemoteUserIsDN|SearchAsUser|SubGroupAttribute|SubGroupClass|Url)|Merging|Name|nCache(?:Context|Enable|ProvideFor|SOCache|Timeout)|nzFcgiCheckAuthnProvider|nzFcgiDefineProvider|Type|UserFile|zDBDLoginToReferer|zDBDQuery|zDBDRedirectQuery|zDBMType|zSendForbiddenOnFailure)|BalancerGrowth|BalancerInherit|BalancerMember|BalancerPersist|BrowserMatch|BrowserMatchNoCase|BufferedLogs|BufferSize|Cache(?:DefaultExpire|DetailHeader|DirLength|DirLevels|Disable|Enable|File|Header|IgnoreCacheControl|IgnoreHeaders|IgnoreNoLastMod|IgnoreQueryString|IgnoreURLSessionIdentifiers|KeyBaseURL|LastModifiedFactor|Lock|LockMaxAge|LockPath|MaxExpire|MaxFileSize|MinExpire|MinFileSize|NegotiatedDocs|QuickHandler|ReadSize|ReadTime|Root|Socache(?:MaxSize|MaxTime|MinTime|ReadSize|ReadTime)?|StaleOnError|StoreExpired|StoreNoStore|StorePrivate)|CGIDScriptTimeout|CGIMapExtension|CharsetDefault|CharsetOptions|CharsetSourceEnc|CheckCaseOnly|CheckSpelling|ChrootDir|ContentDigest|CookieDomain|CookieExpires|CookieName|CookieStyle|CookieTracking|CoreDumpDirectory|CustomLog|Dav|DavDepthInfinity|DavGenericLockDB|DavLockDB|DavMinTimeout|DBDExptime|DBDInitSQL|DBDKeep|DBDMax|DBDMin|DBDParams|DBDPersist|DBDPrepareSQL|DBDriver|DefaultIcon|DefaultLanguage|DefaultRuntimeDir|DefaultType|Define|Deflate(?:BufferSize|CompressionLevel|FilterNote|InflateLimitRequestBody|InflateRatio(?:Burst|Limit)|MemLevel|WindowSize)|Deny|DirectoryCheckHandler|DirectoryIndex|DirectoryIndexRedirect|DirectorySlash|DocumentRoot|DTracePrivileges|DumpIOInput|DumpIOOutput|EnableExceptionHook|EnableMMAP|EnableSendfile|Error|ErrorDocument|ErrorLog|ErrorLogFormat|Example|ExpiresActive|ExpiresByType|ExpiresDefault|ExtendedStatus|ExtFilterDefine|ExtFilterOptions|FallbackResource|FileETag|FilterChain|FilterDeclare|FilterProtocol|FilterProvider|FilterTrace|ForceLanguagePriority|ForceType|ForensicLog|GprofDir|GracefulShutdownTimeout|Group|Header|HeaderName|Heartbeat(?:Address|Listen|MaxServers|Storage)|HostnameLookups|IdentityCheck|IdentityCheckTimeout|ImapBase|ImapDefault|ImapMenu|Include|IncludeOptional|Index(?:HeadInsert|Ignore|IgnoreReset|Options|OrderDefault|StyleSheet)|InputSed|ISAPI(?:AppendLogToErrors|AppendLogToQuery|CacheFile|FakeAsync|LogNotSupported|ReadAheadBuffer)|KeepAlive|KeepAliveTimeout|KeptBodySize|LanguagePriority|LDAP(?:CacheEntries|CacheTTL|ConnectionPoolTTL|ConnectionTimeout|LibraryDebug|OpCacheEntries|OpCacheTTL|ReferralHopLimit|Referrals|Retries|RetryDelay|SharedCacheFile|SharedCacheSize|Timeout|TrustedClientCert|TrustedGlobalCert|TrustedMode|VerifyServerCert)|Limit(?:InternalRecursion|Request(?:Body|Fields|FieldSize|Line)|XMLRequestBody)|Listen|ListenBackLog|LoadFile|LoadModule|LogFormat|LogLevel|LogMessage|LuaAuthzProvider|LuaCodeCache|Lua(?:Hook(?:AccessChecker|AuthChecker|CheckUserID|Fixups|InsertFilter|Log|MapToStorage|TranslateName|TypeChecker)|Inherit|InputFilter|MapHandler|OutputFilter|PackageCPath|PackagePath|QuickHandler|Root|Scope)|Max(?:ConnectionsPerChild|KeepAliveRequests|MemFree|RangeOverlaps|RangeReversals|Ranges|RequestWorkers|SpareServers|SpareThreads|Threads)|MergeTrailers|MetaDir|MetaFiles|MetaSuffix|MimeMagicFile|MinSpareServers|MinSpareThreads|MMapFile|ModemStandard|ModMimeUsePathInfo|MultiviewsMatch|Mutex|NameVirtualHost|NoProxy|NWSSLTrustedCerts|NWSSLUpgradeable|Options|Order|OutputSed|PassEnv|PidFile|PrivilegesMode|Protocol|ProtocolEcho|Proxy(?:AddHeaders|BadHeader|Block|Domain|ErrorOverride|ExpressDBMFile|ExpressDBMType|ExpressEnable|FtpDirCharset|FtpEscapeWildcards|FtpListOnWildcard|HTML(?:BufSize|CharsetOut|DocType|Enable|Events|Extended|Fixups|Interp|Links|Meta|StripComments|URLMap)|IOBufferSize|MaxForwards|Pass(?:Inherit|InterpolateEnv|Match|Reverse|ReverseCookieDomain|ReverseCookiePath)?|PreserveHost|ReceiveBufferSize|Remote|RemoteMatch|Requests|SCGIInternalRedirect|SCGISendfile|Set|SourceAddress|Status|Timeout|Via)|ReadmeName|ReceiveBufferSize|Redirect|RedirectMatch|RedirectPermanent|RedirectTemp|ReflectorHeader|RemoteIP(?:Header|InternalProxy|InternalProxyList|ProxiesHeader|TrustedProxy|TrustedProxyList)|RemoveCharset|RemoveEncoding|RemoveHandler|RemoveInputFilter|RemoveLanguage|RemoveOutputFilter|RemoveType|RequestHeader|RequestReadTimeout|Require|Rewrite(?:Base|Cond|Engine|Map|Options|Rule)|RLimitCPU|RLimitMEM|RLimitNPROC|Satisfy|ScoreBoardFile|Script(?:Alias|AliasMatch|InterpreterSource|Log|LogBuffer|LogLength|Sock)?|SecureListen|SeeRequestTail|SendBufferSize|Server(?:Admin|Alias|Limit|Name|Path|Root|Signature|Tokens)|Session(?:Cookie(?:Name|Name2|Remove)|Crypto(?:Cipher|Driver|Passphrase|PassphraseFile)|DBD(?:CookieName|CookieName2|CookieRemove|DeleteLabel|InsertLabel|PerUser|SelectLabel|UpdateLabel)|Env|Exclude|Header|Include|MaxAge)?|SetEnv|SetEnvIf|SetEnvIfExpr|SetEnvIfNoCase|SetHandler|SetInputFilter|SetOutputFilter|SSIEndTag|SSIErrorMsg|SSIETag|SSILastModified|SSILegacyExprParser|SSIStartTag|SSITimeFormat|SSIUndefinedEcho|SSL(?:CACertificateFile|CACertificatePath|CADNRequestFile|CADNRequestPath|CARevocationCheck|CARevocationFile|CARevocationPath|CertificateChainFile|CertificateFile|CertificateKeyFile|CipherSuite|Compression|CryptoDevice|Engine|FIPS|HonorCipherOrder|InsecureRenegotiation|OCSP(?:DefaultResponder|Enable|OverrideResponder|ResponderTimeout|ResponseMaxAge|ResponseTimeSkew|UseRequestNonce)|OpenSSLConfCmd|Options|PassPhraseDialog|Protocol|Proxy(?:CACertificateFile|CACertificatePath|CARevocation(?:Check|File|Path)|CheckPeer(?:CN|Expire|Name)|CipherSuite|Engine|MachineCertificate(?:ChainFile|File|Path)|Protocol|Verify|VerifyDepth)|RandomSeed|RenegBufferSize|Require|RequireSSL|Session(?:Cache|CacheTimeout|TicketKeyFile|Tickets)|SRPUnknownUserSeed|SRPVerifierFile|Stapling(?:Cache|ErrorCacheTimeout|FakeTryLater|ForceURL|ResponderTimeout|ResponseMaxAge|ResponseTimeSkew|ReturnResponderErrors|StandardCacheTimeout)|StrictSNIVHostCheck|UserName|UseStapling|VerifyClient|VerifyDepth)|StartServers|StartThreads|Substitute|Suexec|SuexecUserGroup|ThreadLimit|ThreadsPerChild|ThreadStackSize|TimeOut|TraceEnable|TransferLog|TypesConfig|UnDefine|UndefMacro|UnsetEnv|Use|UseCanonicalName|UseCanonicalPhysicalPort|User|UserDir|VHostCGIMode|VHostCGIPrivs|VHostGroup|VHostPrivs|VHostSecure|VHostUser|Virtual(?:DocumentRoot|ScriptAlias)(?:IP)?|WatchdogInterval|XBitHack|xml2EncAlias|xml2EncDefault|xml2StartParse)\\b", + caseSensitive: false, + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "directive-block", + compileHighlightPattern( + "<\\/?\\b(?:Auth[nz]ProviderAlias|Directory|DirectoryMatch|Else|ElseIf|Files|FilesMatch|If|IfDefine|IfModule|IfVersion|Limit|LimitExcept|Location|LocationMatch|Macro|Proxy|Require(?:All|Any|None)|VirtualHost)\\b.*>", + caseSensitive: false), + alias: "tag", + inside: () => _g1), + GrammarToken( + "directive-flags", compileHighlightPattern("\\[(?:[\\w=],?)+\\]"), + alias: "keyword"), + GrammarToken("string", compileHighlightPattern("(\"|').*\\1"), + inside: () => _g5), + GrammarToken( + "variable", compileHighlightPattern("[\$%]\\{?(?:\\w\\.?[-+:]?)+\\}?")), + GrammarToken("regex", compileHighlightPattern("\\^?.*\\\$|\\^.*\\\$?")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("directive-block", compileHighlightPattern("^<\\/?\\w+"), + alias: "tag", inside: () => _g2), + GrammarToken("directive-block-parameter", compileHighlightPattern(".*[^>]"), + alias: "attr-value", inside: () => _g3), + GrammarToken("punctuation", compileHighlightPattern(">")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<\\/?")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern(":")), + GrammarToken("string", compileHighlightPattern("(\"|').*\\1"), + inside: () => _g4), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "variable", compileHighlightPattern("[\$%]\\{?(?:\\w\\.?[-+:]?)+\\}?")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "variable", compileHighlightPattern("[\$%]\\{?(?:\\w\\.?[-+:]?)+\\}?")), +]); diff --git a/lib/highlight/bash.dart b/lib/highlight/bash.dart new file mode 100644 index 0000000..c28878e --- /dev/null +++ b/lib/highlight/bash.dart @@ -0,0 +1,285 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `bash`. +/// +/// Import this library only when you need `bash` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightBash { + /// The grammar for `bash`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("shebang", compileHighlightPattern("^#!\\s*\\/.*"), + alias: "important"), + GrammarToken("comment", compileHighlightPattern("(^|[^\"{\\\\\$])#.*"), + lookbehind: true), + GrammarToken( + "function-name", + compileHighlightPattern( + "(\\bfunction\\s+)[\\w-]+(?=(?:\\s*\\(?:\\s*\\))?\\s*\\{)"), + lookbehind: true, + alias: "function"), + GrammarToken("function-name", + compileHighlightPattern("\\b[\\w-]+(?=\\s*\\(\\s*\\)\\s*\\{)"), + alias: "function"), + GrammarToken("for-or-select", + compileHighlightPattern("(\\b(?:for|select)\\s+)\\w+(?=\\s+in\\s)"), + lookbehind: true, alias: "variable"), + GrammarToken("assign-left", + compileHighlightPattern("(^|[\\s;|&]|[<>]\\()\\w+(?:\\.\\w+)*(?=\\+?=)"), + lookbehind: true, alias: "variable", inside: () => _g1), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|\\s)-{1,2}(?:\\w+:[+-]?)?\\w+(?:\\.\\w+)*(?=[=\\s]|\$)"), + lookbehind: true, + alias: "variable"), + GrammarToken( + "string", + compileHighlightPattern( + "((?:^|[^<])<<-?\\s*)(\\w+)\\s[\\s\\S]*?(?:\\r?\\n|\\r)\\2"), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "string", + compileHighlightPattern( + "((?:^|[^<])<<-?\\s*)([\"'])(\\w+)\\2\\s[\\s\\S]*?(?:\\r?\\n|\\r)\\3"), + lookbehind: true, + greedy: true, + inside: () => _g5), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^\\\\](?:\\\\\\\\)*)\"(?:\\\\[\\s\\S]|\\\$\\([^)]+\\)|\\\$(?!\\()|`[^`]+`|[^\"\\\\`\$])*\""), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("(^|[^\$\\\\])'[^']*'"), + lookbehind: true, greedy: true), + GrammarToken( + "string", compileHighlightPattern("\\\$'(?:[^'\\\\]|\\\\[\\s\\S])*'"), + greedy: true, inside: () => _g6), + GrammarToken( + "environment", + compileHighlightPattern( + "\\\$?\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b"), + alias: "constant"), + GrammarToken( + "variable", compileHighlightPattern("\\\$?\\(\\([\\s\\S]+?\\)\\)"), + greedy: true, inside: () => _g3), + GrammarToken("variable", + compileHighlightPattern("\\\$\\((?:\\([^)]+\\)|[^()])+\\)|`[^`]+`"), + greedy: true, inside: () => _g4), + GrammarToken("variable", compileHighlightPattern("\\\$\\{[^}]+\\}"), + greedy: true, inside: () => _g8), + GrammarToken("variable", compileHighlightPattern("\\\$(?:\\w+|[#?*!@\$])")), + GrammarToken( + "function", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=\$|[)\\s;|&])"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=\$|[)\\s;|&])"), + lookbehind: true), + GrammarToken( + "builtin", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:\\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=\$|[)\\s;|&])"), + lookbehind: true, + alias: "class-name"), + GrammarToken( + "boolean", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:false|true)(?=\$|[)\\s;|&])"), + lookbehind: true), + GrammarToken("file-descriptor", compileHighlightPattern("\\B&\\d\\b"), + alias: "important"), + GrammarToken( + "operator", + compileHighlightPattern( + "\\d?<>|>\\||\\+=|=[=~]?|!=?|<<[<-]?|[&\\d]?>>|\\d[<>]&?|[<>][&=]?|&[>&]?|\\|[&|]?"), + inside: () => _g7), + GrammarToken("punctuation", + compileHighlightPattern("\\\$?\\(\\(?|\\)\\)?|\\.\\.|[{}[\\];\\\\]")), + GrammarToken("number", + compileHighlightPattern("(^|\\s)(?:[1-9]\\d*|0)(?:[.,]\\d+)?\\b"), + lookbehind: true), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "environment", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b"), + lookbehind: true, + alias: "constant"), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "bash", compileHighlightPattern("(^([\"']?)\\w+\\2)[ \\t]+\\S.*"), + lookbehind: true, alias: "punctuation", inside: () => _g0), + GrammarToken( + "environment", + compileHighlightPattern( + "\\\$\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b"), + alias: "constant"), + GrammarToken( + "variable", compileHighlightPattern("\\\$?\\(\\([\\s\\S]+?\\)\\)"), + greedy: true, inside: () => _g3), + GrammarToken("variable", + compileHighlightPattern("\\\$\\((?:\\([^)]+\\)|[^()])+\\)|`[^`]+`"), + greedy: true, inside: () => _g4), + GrammarToken("variable", compileHighlightPattern("\\\$\\{[^}]+\\}"), + greedy: true, inside: () => _g8), + GrammarToken("variable", compileHighlightPattern("\\\$(?:\\w+|[#?*!@\$])")), + GrammarToken( + "entity", + compileHighlightPattern( + "\\\\(?:[abceEfnrtv\\\\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "variable", compileHighlightPattern("(^\\\$\\(\\([\\s\\S]+)\\)\\)"), + lookbehind: true), + GrammarToken("variable", compileHighlightPattern("^\\\$\\(\\(")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\dA-Fa-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[Ee]-?\\d+)?")), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|<<=?|>>=?|&&|\\|\\||[=!+\\-*/%<>^&|]=?|[?~:]")), + GrammarToken("punctuation", compileHighlightPattern("\\(\\(?|\\)\\)?|,|;")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("variable", compileHighlightPattern("^\\\$\\(|^`|\\)\$|`\$")), + GrammarToken("comment", compileHighlightPattern("(^|[^\"{\\\\\$])#.*"), + lookbehind: true), + GrammarToken( + "function-name", + compileHighlightPattern( + "(\\bfunction\\s+)[\\w-]+(?=(?:\\s*\\(?:\\s*\\))?\\s*\\{)"), + lookbehind: true, + alias: "function"), + GrammarToken("function-name", + compileHighlightPattern("\\b[\\w-]+(?=\\s*\\(\\s*\\)\\s*\\{)"), + alias: "function"), + GrammarToken("for-or-select", + compileHighlightPattern("(\\b(?:for|select)\\s+)\\w+(?=\\s+in\\s)"), + lookbehind: true, alias: "variable"), + GrammarToken("assign-left", + compileHighlightPattern("(^|[\\s;|&]|[<>]\\()\\w+(?:\\.\\w+)*(?=\\+?=)"), + lookbehind: true, alias: "variable", inside: () => _g1), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|\\s)-{1,2}(?:\\w+:[+-]?)?\\w+(?:\\.\\w+)*(?=[=\\s]|\$)"), + lookbehind: true, + alias: "variable"), + GrammarToken( + "string", + compileHighlightPattern( + "((?:^|[^<])<<-?\\s*)(\\w+)\\s[\\s\\S]*?(?:\\r?\\n|\\r)\\2"), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "string", + compileHighlightPattern( + "((?:^|[^<])<<-?\\s*)([\"'])(\\w+)\\2\\s[\\s\\S]*?(?:\\r?\\n|\\r)\\3"), + lookbehind: true, + greedy: true, + inside: () => _g5), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^\\\\](?:\\\\\\\\)*)\"(?:\\\\[\\s\\S]|\\\$\\([^)]+\\)|\\\$(?!\\()|`[^`]+`|[^\"\\\\`\$])*\""), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("(^|[^\$\\\\])'[^']*'"), + lookbehind: true, greedy: true), + GrammarToken( + "string", compileHighlightPattern("\\\$'(?:[^'\\\\]|\\\\[\\s\\S])*'"), + greedy: true, inside: () => _g6), + GrammarToken( + "environment", + compileHighlightPattern( + "\\\$?\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b"), + alias: "constant"), + GrammarToken( + "function", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=\$|[)\\s;|&])"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=\$|[)\\s;|&])"), + lookbehind: true), + GrammarToken( + "builtin", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:\\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=\$|[)\\s;|&])"), + lookbehind: true, + alias: "class-name"), + GrammarToken( + "boolean", + compileHighlightPattern( + "(^|[\\s;|&]|[<>]\\()(?:false|true)(?=\$|[)\\s;|&])"), + lookbehind: true), + GrammarToken("file-descriptor", compileHighlightPattern("\\B&\\d\\b"), + alias: "important"), + GrammarToken( + "operator", + compileHighlightPattern( + "\\d?<>|>\\||\\+=|=[=~]?|!=?|<<[<-]?|[&\\d]?>>|\\d[<>]&?|[<>][&=]?|&[>&]?|\\|[&|]?"), + inside: () => _g7), + GrammarToken("punctuation", + compileHighlightPattern("\\\$?\\(\\(?|\\)\\)?|\\.\\.|[{}[\\];\\\\]")), + GrammarToken("number", + compileHighlightPattern("(^|\\s)(?:[1-9]\\d*|0)(?:[.,]\\d+)?\\b"), + lookbehind: true), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "bash", compileHighlightPattern("(^([\"']?)\\w+\\2)[ \\t]+\\S.*"), + lookbehind: true, alias: "punctuation", inside: () => _g0), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "entity", + compileHighlightPattern( + "\\\\(?:[abceEfnrtv\\\\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("file-descriptor", compileHighlightPattern("^\\d"), + alias: "important"), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("operator", + compileHighlightPattern(":[-=?+]?|[!\\/]|##?|%%?|\\^\\^?|,,?")), + GrammarToken("punctuation", compileHighlightPattern("[\\[\\]]")), + GrammarToken( + "environment", + compileHighlightPattern( + "(\\{)\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b"), + lookbehind: true, + alias: "constant"), +]); diff --git a/lib/highlight/batch.dart b/lib/highlight/batch.dart new file mode 100644 index 0000000..41828a6 --- /dev/null +++ b/lib/highlight/batch.dart @@ -0,0 +1,151 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `batch`. +/// +/// Import this library only when you need `batch` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightBatch { + /// The grammar for `batch`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("^::.*", multiLine: true)), + GrammarToken( + "comment", + compileHighlightPattern( + "((?:^|[&(])[ \\t]*)rem\\b(?:[^^&)\\r\\n]|\\^(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true), + GrammarToken("label", compileHighlightPattern("^:.*", multiLine: true), + alias: "property"), + GrammarToken( + "command", + compileHighlightPattern( + "((?:^|[&(])[ \\t]*)for(?: \\/[a-z?](?:[ :](?:\"[^\"]*\"|[^\\s\"/]\\S*))?)* \\S+ in \\([^)]+\\) do", + caseSensitive: false, + multiLine: true), + lookbehind: true, + inside: () => _g1), + GrammarToken( + "command", + compileHighlightPattern( + "((?:^|[&(])[ \\t]*)if(?: \\/[a-z?](?:[ :](?:\"[^\"]*\"|[^\\s\"/]\\S*))?)* (?:not )?(?:cmdextversion \\d+|defined \\w+|errorlevel \\d+|exist \\S+|(?:\"[^\"]*\"|(?!\")(?:(?!==)\\S)+)?(?:==| (?:equ|geq|gtr|leq|lss|neq) )(?:\"[^\"]*\"|[^\\s\"]\\S*))", + caseSensitive: false, + multiLine: true), + lookbehind: true, + inside: () => _g3), + GrammarToken( + "command", + compileHighlightPattern("((?:^|[&()])[ \\t]*)else\\b", + caseSensitive: false, multiLine: true), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "command", + compileHighlightPattern( + "((?:^|[&(])[ \\t]*)set(?: \\/[a-z](?:[ :](?:\"[^\"]*\"|[^\\s\"/]\\S*))?)* (?:[^^&)\\r\\n]|\\^(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + inside: () => _g5), + GrammarToken( + "command", + compileHighlightPattern( + "((?:^|[&(])[ \\t]*@?)\\w+\\b(?:\"(?:[\\\\\"]\"|[^\"])*\"(?!\")|[^\"^&)\\r\\n]|\\^(?:\\r\\n|[\\s\\S]))*", + multiLine: true), + lookbehind: true, + inside: () => _g6), + GrammarToken("operator", compileHighlightPattern("[&@]")), + GrammarToken("punctuation", compileHighlightPattern("[()']")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("keyword", + compileHighlightPattern("\\b(?:do|in)\\b|^for\\b", caseSensitive: false)), + GrammarToken( + "string", compileHighlightPattern("\"(?:[\\\\\"]\"|[^\"])*\"(?!\")")), + GrammarToken( + "parameter", + compileHighlightPattern("\\/[a-z?]+(?=[ :]|\$):?|-[a-z]\\b|--[a-z-]+\\b", + caseSensitive: false, multiLine: true), + alias: "attr-name", + inside: () => _g2), + GrammarToken("variable", compileHighlightPattern("%%?[~:\\w]+%?|!\\S+!")), + GrammarToken("number", compileHighlightPattern("(?:\\b|-)\\d+\\b")), + GrammarToken("punctuation", compileHighlightPattern("[()',]")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern(":")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:cmdextversion|defined|errorlevel|exist|not)\\b|^if\\b", + caseSensitive: false)), + GrammarToken( + "string", compileHighlightPattern("\"(?:[\\\\\"]\"|[^\"])*\"(?!\")")), + GrammarToken( + "parameter", + compileHighlightPattern("\\/[a-z?]+(?=[ :]|\$):?|-[a-z]\\b|--[a-z-]+\\b", + caseSensitive: false, multiLine: true), + alias: "attr-name", + inside: () => _g2), + GrammarToken("variable", compileHighlightPattern("%%?[~:\\w]+%?|!\\S+!")), + GrammarToken("number", compileHighlightPattern("(?:\\b|-)\\d+\\b")), + GrammarToken( + "operator", + compileHighlightPattern("\\^|==|\\b(?:equ|geq|gtr|leq|lss|neq)\\b", + caseSensitive: false)), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "keyword", compileHighlightPattern("^else\\b", caseSensitive: false)), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "keyword", compileHighlightPattern("^set\\b", caseSensitive: false)), + GrammarToken( + "string", compileHighlightPattern("\"(?:[\\\\\"]\"|[^\"])*\"(?!\")")), + GrammarToken( + "parameter", + compileHighlightPattern("\\/[a-z?]+(?=[ :]|\$):?|-[a-z]\\b|--[a-z-]+\\b", + caseSensitive: false, multiLine: true), + alias: "attr-name", + inside: () => _g2), + GrammarToken("variable", compileHighlightPattern("%%?[~:\\w]+%?|!\\S+!")), + GrammarToken("variable", + compileHighlightPattern("\\w+(?=(?:[*\\/%+\\-&^|]|<<|>>)?=)")), + GrammarToken("number", compileHighlightPattern("(?:\\b|-)\\d+\\b")), + GrammarToken( + "operator", compileHighlightPattern("[*\\/%+\\-&^|]=?|<<=?|>>=?|[!~_=]")), + GrammarToken("punctuation", compileHighlightPattern("[()',]")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken("keyword", compileHighlightPattern("^\\w+\\b")), + GrammarToken( + "string", compileHighlightPattern("\"(?:[\\\\\"]\"|[^\"])*\"(?!\")")), + GrammarToken( + "parameter", + compileHighlightPattern("\\/[a-z?]+(?=[ :]|\$):?|-[a-z]\\b|--[a-z-]+\\b", + caseSensitive: false, multiLine: true), + alias: "attr-name", + inside: () => _g2), + GrammarToken( + "label", compileHighlightPattern("(^\\s*):\\S+", multiLine: true), + lookbehind: true, alias: "property"), + GrammarToken("variable", compileHighlightPattern("%%?[~:\\w]+%?|!\\S+!")), + GrammarToken("number", compileHighlightPattern("(?:\\b|-)\\d+\\b")), + GrammarToken("operator", compileHighlightPattern("\\^")), +]); diff --git a/lib/highlight/c.dart b/lib/highlight/c.dart new file mode 100644 index 0000000..47f4cd0 --- /dev/null +++ b/lib/highlight/c.dart @@ -0,0 +1,106 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `c`. +/// +/// Import this library only when you need `c` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightC { + /// The grammar for `c`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "macro", + compileHighlightPattern( + "(^[\\t ]*)#\\s*[a-z](?:[^\\r\\n\\\\/]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\\\\(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property", + inside: () => _g1), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:enum|struct)\\s+(?:__attribute__\\s*\\(\\([\\s\\S]*?\\)\\)\\s*)?)\\w+|\\b[a-z]\\w*_t\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\\b")), + GrammarToken( + "constant", + compileHighlightPattern( + "\\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0x(?:[\\da-f]+(?:\\.[\\da-f]*)?|\\.[\\da-f]+)(?:p[+-]?\\d+)?|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?)[ful]{0,4}", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + ">>=?|<<=?|->|([-+&|:])\\1|[?:~]|[-+*/%&|^!=<>]=?")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("string", compileHighlightPattern("^(#\\s*include\\s*)<[^>]+>"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?!\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?=\\()", + caseSensitive: false), + lookbehind: true, + alias: "function"), + GrammarToken("directive", compileHighlightPattern("^(#\\s*)[a-z]+"), + lookbehind: true, alias: "keyword"), + GrammarToken("directive-hash", compileHighlightPattern("^#")), + GrammarToken("punctuation", compileHighlightPattern("##|\\\\(?=[\\r\\n])")), + GrammarToken("expression", compileHighlightPattern("\\S[\\s\\S]*"), + inside: () => _g0), +]); diff --git a/lib/highlight/clike.dart b/lib/highlight/clike.dart new file mode 100644 index 0000000..1eef8eb --- /dev/null +++ b/lib/highlight/clike.dart @@ -0,0 +1,53 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `clike`. +/// +/// Import this library only when you need `clike` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightClike { + /// The grammar for `clike`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g1), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\da-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "[<>]=?|[!=]=?=?|--?|\\+\\+?|&&?|\\|\\|?|[?*/~^%]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); diff --git a/lib/highlight/clojure.dart b/lib/highlight/clojure.dart new file mode 100644 index 0000000..1d05bac --- /dev/null +++ b/lib/highlight/clojure.dart @@ -0,0 +1,41 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `clojure`. +/// +/// Import this library only when you need `clojure` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightClojure { + /// The grammar for `clojure`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern(";.*"), greedy: true), + GrammarToken("string", compileHighlightPattern("\"(?:[^\"\\\\]|\\\\.)*\""), + greedy: true), + GrammarToken("char", compileHighlightPattern("\\\\\\w+")), + GrammarToken("symbol", + compileHighlightPattern("(^|[\\s()\\[\\]{},])::?[\\w*+!?'<>=/.-]+"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(\\()(?:-|->|->>|\\.|\\.\\.|\\*|\\/|\\+|<|<=|=|==|>|>=|accessor|agent|agent-errors|aget|alength|all-ns|alter|and|append-child|apply|array-map|aset|aset-boolean|aset-byte|aset-char|aset-double|aset-float|aset-int|aset-long|aset-short|assert|assoc|await|await-for|bean|binding|bit-and|bit-not|bit-or|bit-shift-left|bit-shift-right|bit-xor|boolean|branch\\?|butlast|byte|cast|char|children|class|clear-agent-errors|comment|commute|comp|comparator|complement|concat|cond|conj|cons|constantly|construct-proxy|contains\\?|count|create-ns|create-struct|cycle|dec|declare|def|def-|definline|definterface|defmacro|defmethod|defmulti|defn|defn-|defonce|defproject|defprotocol|defrecord|defstruct|deftype|deref|difference|disj|dissoc|distinct|do|doall|doc|dorun|doseq|dosync|dotimes|doto|double|down|drop|drop-while|edit|end\\?|ensure|eval|every\\?|false\\?|ffirst|file-seq|filter|find|find-doc|find-ns|find-var|first|float|flush|fn|fnseq|for|frest|gensym|get|get-proxy-class|hash-map|hash-set|identical\\?|identity|if|if-let|if-not|import|in-ns|inc|index|insert-child|insert-left|insert-right|inspect-table|inspect-tree|instance\\?|int|interleave|intersection|into|into-array|iterate|join|key|keys|keyword|keyword\\?|last|lazy-cat|lazy-cons|left|lefts|let|line-seq|list|list\\*|load|load-file|locking|long|loop|macroexpand|macroexpand-1|make-array|make-node|map|map-invert|map\\?|mapcat|max|max-key|memfn|merge|merge-with|meta|min|min-key|monitor-enter|name|namespace|neg\\?|new|newline|next|nil\\?|node|not|not-any\\?|not-every\\?|not=|ns|ns-imports|ns-interns|ns-map|ns-name|ns-publics|ns-refers|ns-resolve|ns-unmap|nth|nthrest|or|parse|partial|path|peek|pop|pos\\?|pr|pr-str|print|print-str|println|println-str|prn|prn-str|project|proxy|proxy-mappings|quot|quote|rand|rand-int|range|re-find|re-groups|re-matcher|re-matches|re-pattern|re-seq|read|read-line|recur|reduce|ref|ref-set|refer|rem|remove|remove-method|remove-ns|rename|rename-keys|repeat|replace|replicate|resolve|rest|resultset-seq|reverse|rfirst|right|rights|root|rrest|rseq|second|select|select-keys|send|send-off|seq|seq-zip|seq\\?|set|set!|short|slurp|some|sort|sort-by|sorted-map|sorted-map-by|sorted-set|special-symbol\\?|split-at|split-with|str|string\\?|struct|struct-map|subs|subvec|symbol|symbol\\?|sync|take|take-nth|take-while|test|throw|time|to-array|to-array-2d|tree-seq|true\\?|try|union|up|update-proxy|val|vals|var|var-get|var-set|var\\?|vector|vector-zip|vector\\?|when|when-first|when-let|when-not|with-local-vars|with-meta|with-open|with-out-str|xml-seq|xml-zip|zero\\?|zipmap|zipper)(?=[\\s)]|\$)"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|nil|true)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$@])(?:\\d+(?:[/.]\\d+)?(?:e[+-]?\\d+)?|0x[a-f0-9]+|[1-9]\\d?r[a-z0-9]+)[lmn]?(?![\\w\$@])", + caseSensitive: false), + lookbehind: true), + GrammarToken("function", + compileHighlightPattern("((?:^|[^'])\\()[\\w*+!?'<>=/.-]+(?=[\\s)]|\$)"), + lookbehind: true), + GrammarToken("operator", compileHighlightPattern("[#@^`~]")), + GrammarToken("punctuation", compileHighlightPattern("[{}\\[\\](),]")), +]); diff --git a/lib/highlight/coffeescript.dart b/lib/highlight/coffeescript.dart new file mode 100644 index 0000000..34cf9b7 --- /dev/null +++ b/lib/highlight/coffeescript.dart @@ -0,0 +1,244 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'js.dart'; + +/// Syntax grammar for `coffeescript`. +/// +/// Import this library only when you need `coffeescript` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightCoffeescript { + /// The grammar for `coffeescript`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("multiline-comment", compileHighlightPattern("###[\\s\\S]+?###"), + alias: "comment"), + GrammarToken("block-regex", compileHighlightPattern("\\/{3}[\\s\\S]*?\\/{3}"), + alias: "regex", inside: () => _g1), + GrammarToken("comment", compileHighlightPattern("#(?!\\{).+")), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken("inline-javascript", + compileHighlightPattern("`(?:\\\\[\\s\\S]|[^\\\\`])*`"), + inside: () => _g2), + GrammarToken("multiline-string", compileHighlightPattern("'''[\\s\\S]*?'''"), + greedy: true, alias: "string"), + GrammarToken( + "multiline-string", compileHighlightPattern("\"\"\"[\\s\\S]*?\"\"\""), + greedy: true, alias: "string", inside: () => _g3), + GrammarToken( + "string", compileHighlightPattern("'(?:\\\\[\\s\\S]|[^\\\\'])*'"), + greedy: true), + GrammarToken( + "string", compileHighlightPattern("\"(?:\\\\[\\s\\S]|[^\\\\\"])*\""), + greedy: true, inside: () => _g4), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g5), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g6), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken( + "property", compileHighlightPattern("(?!\\d)\\w+(?=\\s*:(?!:))")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken("class-member", compileHighlightPattern("@(?!\\d)\\w+"), + alias: "variable"), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("comment", compileHighlightPattern("#(?!\\{).+")), + GrammarToken("interpolation", compileHighlightPattern("#\\{[^}]+\\}"), + alias: "variable"), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("delimiter", compileHighlightPattern("^`|`\$"), + alias: "punctuation"), + GrammarToken("script", compileHighlightPattern("[\\s\\S]+"), + alias: "language-javascript", inside: () => HighlightJs.grammar), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("interpolation", compileHighlightPattern("#\\{[^}]+\\}"), + alias: "variable"), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("interpolation", compileHighlightPattern("#\\{[^}]+\\}"), + alias: "variable"), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g7), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g10), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g11), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g9), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g9 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g11 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); diff --git a/lib/highlight/cpp.dart b/lib/highlight/cpp.dart new file mode 100644 index 0000000..7925cbf --- /dev/null +++ b/lib/highlight/cpp.dart @@ -0,0 +1,363 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `cpp`. +/// +/// Import this library only when you need `cpp` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightCpp { + /// The grammar for `cpp`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "macro", + compileHighlightPattern( + "(^[\\t ]*)#\\s*[a-z](?:[^\\r\\n\\\\/]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\\\\(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property", + inside: () => _g1), + GrammarToken( + "module", + compileHighlightPattern( + "(\\b(?:import|module)\\s+)(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|<[^<>\\r\\n]*>|\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b(?:\\s*:\\s*\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b)?|:\\s*\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b)"), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken("raw-string", + compileHighlightPattern("R\"([^()\\\\ ]{0,16})\\([\\s\\S]*?\\)\\1\""), + greedy: true, alias: "string"), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "base-clause", + compileHighlightPattern( + "(\\b(?:class|struct)\\s+\\w+\\s*:\\s*)[^;{}\"'\\s]+(?:\\s+[^;{}\"'\\s]+)*(?=\\s*[;{])"), + lookbehind: true, + greedy: true, + inside: () => _g3), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|concept|enum|struct|typename)\\s+)(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+"), + lookbehind: true), + GrammarToken("class-name", + compileHighlightPattern("\\b[A-Z]\\w*(?=\\s*::\\s*\\w+\\s*\\()")), + GrammarToken( + "class-name", + compileHighlightPattern("\\b[A-Z_]\\w*(?=\\s*::\\s*~\\w+\\s*\\()", + caseSensitive: false)), + GrammarToken( + "class-name", + compileHighlightPattern( + "\\b\\w+(?=\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\\s*::\\s*\\w+\\s*\\()")), + GrammarToken( + "generic-function", + compileHighlightPattern( + "\\b(?!operator\\b)[a-z_]\\w*\\s*<(?:[^<>]|<[^<>]*>)*>(?=\\s*\\()", + caseSensitive: false), + inside: () => _g8), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b")), + GrammarToken( + "constant", + compileHighlightPattern( + "\\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0b[01']+|\\b0x(?:[\\da-f']+(?:\\.[\\da-f']*)?|\\.[\\da-f']+)(?:p[+-]?[\\d']+)?|(?:\\b[\\d']+(?:\\.[\\d']*)?|\\B\\.[\\d']+)(?:e[+-]?[\\d']+)?)[ful]{0,4}", + caseSensitive: false), + greedy: true), + GrammarToken("double-colon", compileHighlightPattern("::"), + alias: "punctuation"), + GrammarToken( + "operator", + compileHighlightPattern( + ">>=?|<<=?|->|--|\\+\\+|&&|\\|\\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("string", compileHighlightPattern("^(#\\s*include\\s*)<[^>]+>"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?!\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?=\\()", + caseSensitive: false), + lookbehind: true, + alias: "function"), + GrammarToken("directive", compileHighlightPattern("^(#\\s*)[a-z]+"), + lookbehind: true, alias: "keyword"), + GrammarToken("directive-hash", compileHighlightPattern("^#")), + GrammarToken("punctuation", compileHighlightPattern("##|\\\\(?=[\\r\\n])")), + GrammarToken("expression", compileHighlightPattern("\\S[\\s\\S]*"), + inside: () => _g0), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("string", compileHighlightPattern("^[<\"][\\s\\S]+")), + GrammarToken("operator", compileHighlightPattern(":")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "macro", + compileHighlightPattern( + "(^[\\t ]*)#\\s*[a-z](?:[^\\r\\n\\\\/]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\\\\(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property", + inside: () => _g4), + GrammarToken( + "module", + compileHighlightPattern( + "(\\b(?:import|module)\\s+)(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|<[^<>\\r\\n]*>|\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b(?:\\s*:\\s*\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b)?|:\\s*\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b)"), + lookbehind: true, + greedy: true, + inside: () => _g6), + GrammarToken("raw-string", + compileHighlightPattern("R\"([^()\\\\ ]{0,16})\\([\\s\\S]*?\\)\\1\""), + greedy: true, alias: "string"), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "generic-function", + compileHighlightPattern( + "\\b(?!operator\\b)[a-z_]\\w*\\s*<(?:[^<>]|<[^<>]*>)*>(?=\\s*\\()", + caseSensitive: false), + inside: () => _g7), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b")), + GrammarToken( + "constant", + compileHighlightPattern( + "\\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0b[01']+|\\b0x(?:[\\da-f']+(?:\\.[\\da-f']*)?|\\.[\\da-f']+)(?:p[+-]?[\\d']+)?|(?:\\b[\\d']+(?:\\.[\\d']*)?|\\B\\.[\\d']+)(?:e[+-]?[\\d']+)?)[ful]{0,4}", + caseSensitive: false), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern("\\b[a-z_]\\w*\\b(?!\\s*::)", + caseSensitive: false)), + GrammarToken("double-colon", compileHighlightPattern("::"), + alias: "punctuation"), + GrammarToken( + "operator", + compileHighlightPattern( + ">>=?|<<=?|->|--|\\+\\+|&&|\\|\\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("string", compileHighlightPattern("^(#\\s*include\\s*)<[^>]+>"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?!\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?=\\()", + caseSensitive: false), + lookbehind: true, + alias: "function"), + GrammarToken("directive", compileHighlightPattern("^(#\\s*)[a-z]+"), + lookbehind: true, alias: "keyword"), + GrammarToken("directive-hash", compileHighlightPattern("^#")), + GrammarToken("punctuation", compileHighlightPattern("##|\\\\(?=[\\r\\n])")), + GrammarToken("expression", compileHighlightPattern("\\S[\\s\\S]*"), + inside: () => _g5), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "macro", + compileHighlightPattern( + "(^[\\t ]*)#\\s*[a-z](?:[^\\r\\n\\\\/]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\\\\(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property", + inside: () => _g4), + GrammarToken( + "module", + compileHighlightPattern( + "(\\b(?:import|module)\\s+)(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|<[^<>\\r\\n]*>|\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b(?:\\s*:\\s*\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b)?|:\\s*\\b(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+(?:\\s*\\.\\s*\\w+)*\\b)"), + lookbehind: true, + greedy: true, + inside: () => _g6), + GrammarToken("raw-string", + compileHighlightPattern("R\"([^()\\\\ ]{0,16})\\([\\s\\S]*?\\)\\1\""), + greedy: true, alias: "string"), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|concept|enum|struct|typename)\\s+)(?!\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b)\\w+"), + lookbehind: true), + GrammarToken("class-name", + compileHighlightPattern("\\b[A-Z]\\w*(?=\\s*::\\s*\\w+\\s*\\()")), + GrammarToken( + "class-name", + compileHighlightPattern("\\b[A-Z_]\\w*(?=\\s*::\\s*~\\w+\\s*\\()", + caseSensitive: false)), + GrammarToken( + "class-name", + compileHighlightPattern( + "\\b\\w+(?=\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\\s*::\\s*\\w+\\s*\\()")), + GrammarToken( + "generic-function", + compileHighlightPattern( + "\\b(?!operator\\b)[a-z_]\\w*\\s*<(?:[^<>]|<[^<>]*>)*>(?=\\s*\\()", + caseSensitive: false), + inside: () => _g7), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\\b")), + GrammarToken( + "constant", + compileHighlightPattern( + "\\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0b[01']+|\\b0x(?:[\\da-f']+(?:\\.[\\da-f']*)?|\\.[\\da-f']+)(?:p[+-]?[\\d']+)?|(?:\\b[\\d']+(?:\\.[\\d']*)?|\\B\\.[\\d']+)(?:e[+-]?[\\d']+)?)[ful]{0,4}", + caseSensitive: false), + greedy: true), + GrammarToken("double-colon", compileHighlightPattern("::"), + alias: "punctuation"), + GrammarToken( + "operator", + compileHighlightPattern( + ">>=?|<<=?|->|--|\\+\\+|&&|\\|\\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken("string", compileHighlightPattern("^[<\"][\\s\\S]+")), + GrammarToken("operator", compileHighlightPattern(":")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("function", compileHighlightPattern("^\\w+")), + GrammarToken("generic", compileHighlightPattern("<[\\s\\S]+"), + alias: "class-name", inside: () => _g5), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("function", compileHighlightPattern("^\\w+")), + GrammarToken("generic", compileHighlightPattern("<[\\s\\S]+"), + alias: "class-name", inside: () => _g0), +]); diff --git a/lib/highlight/csharp.dart b/lib/highlight/csharp.dart new file mode 100644 index 0000000..5bddbeb --- /dev/null +++ b/lib/highlight/csharp.dart @@ -0,0 +1,292 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `csharp`. +/// +/// Import this library only when you need `csharp` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightCsharp { + /// The grammar for `csharp`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "interpolation-string", + compileHighlightPattern( + "(^|[^\\\\])(?:\\\$@|@\\\$)\"(?:\"\"|\\\\[\\s\\S]|\\{\\{|(?:\\{(?!\\{)(?:(?![}:])(?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\([^\\s\\S]*\\))*\\))*\\))*\\)))*(?::[^}\\r\\n]+)?\\})|[^\\\\{\"])*\""), + lookbehind: true, + greedy: true, + inside: () => _g1), + GrammarToken( + "interpolation-string", + compileHighlightPattern( + "(^|[^@\\\\])\\\$\"(?:\\\\.|\\{\\{|(?:\\{(?!\\{)(?:(?![}:])(?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\([^\\s\\S]*\\))*\\))*\\))*\\)))*(?::[^}\\r\\n]+)?\\})|[^\\\\\"{])*\""), + lookbehind: true, + greedy: true, + inside: () => _g4), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'"), + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^\$\\\\])(?:@\"(?:\"\"|\\\\[\\s\\S]|[^\\\\\"])*\"(?!\"))"), + lookbehind: true, + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^@\$\\\\])(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\")"), + lookbehind: true, + greedy: true), + GrammarToken( + "namespace", + compileHighlightPattern( + "(\\b(?:namespace|using)\\s+)(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*\\.\\s*(?:@?\\b[A-Za-z_]\\w*\\b))*(?=\\s*[;{])"), + lookbehind: true, + inside: () => _g7), + GrammarToken( + "type-expression", + compileHighlightPattern( + "(\\b(?:default|sizeof|typeof)\\s*\\(\\s*(?!\\s))(?:[^()\\s]|\\s(?!\\s)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\)))*(?=\\s*\\))"), + lookbehind: true, + alias: "class-name", + inside: () => _g8), + GrammarToken( + "return-type", + compileHighlightPattern( + "(?:(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)(?=\\s+(?:(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*)\\s*(?:=>|[({]|\\.\\s*this\\s*\\[)|this\\s*\\[))"), + alias: "class-name", + inside: () => _g8), + GrammarToken( + "constructor-invocation", + compileHighlightPattern( + "(\\bnew\\s+)(?:(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)(?=\\s*[[({])"), + lookbehind: true, + alias: "class-name", + inside: () => _g8), + GrammarToken( + "generic-method", + compileHighlightPattern( + "(?:@?\\b[A-Za-z_]\\w*\\b)\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)(?=\\s*\\()"), + inside: () => _g9), + GrammarToken( + "type-list", + compileHighlightPattern( + "\\b((?:(?:\\b(?:class|enum|interface|record|struct)\\b)\\s+(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)|record\\s+(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)\\s*(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|where\\s+(?:@?\\b[A-Za-z_]\\w*\\b))\\s*:\\s*)(?:(?:(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)|(?:\\b(?:bool|byte|char|decimal|double|dynamic|float|int|long|object|sbyte|short|string|uint|ulong|ushort|var|void|class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b)|(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)\\s*(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\bnew\\s*\\(\\s*\\)))(?:\\s*,\\s*(?:(?:(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)|(?:\\b(?:bool|byte|char|decimal|double|dynamic|float|int|long|object|sbyte|short|string|uint|ulong|ushort|var|void|class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b)|(?:\\bnew\\s*\\(\\s*\\))))*(?=\\s*(?:where|[{;]|=>|\$))"), + lookbehind: true, + inside: () => _g10), + GrammarToken( + "preprocessor", compileHighlightPattern("(^[\\t ]*)#.*", multiLine: true), + lookbehind: true, alias: "property", inside: () => _g11), + GrammarToken( + "attribute", + compileHighlightPattern( + "((?:^|[^\\s\\w>)?])\\s*\\[\\s*)(?:(?:\\b(?:assembly|event|field|method|module|param|property|return|type)\\b)\\s*:\\s*)?(?:(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*)(?:\\s*\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\([^\\s\\S]*\\))*\\))*\\))*\\))*\\))?)(?:\\s*,\\s*(?:(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*)(?:\\s*\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\([^\\s\\S]*\\))*\\))*\\))*\\))*\\))?))*(?=\\s*\\])"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\busing\\s+static\\s+)(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*)(?=\\s*;)"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\busing\\s+(?:@?\\b[A-Za-z_]\\w*\\b)\\s*=\\s*)(?:(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)(?=\\s*;)"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\busing\\s+)(?:@?\\b[A-Za-z_]\\w*\\b)(?=\\s*=)"), + lookbehind: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:\\b(?:class|enum|interface|record|struct)\\b)\\s+)(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\bcatch\\s*\\(\\s*)(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*)"), + lookbehind: true, + inside: () => _g8), + GrammarToken("class-name", + compileHighlightPattern("(\\bwhere\\s+)(?:@?\\b[A-Za-z_]\\w*\\b)"), + lookbehind: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:is(?:\\s+not)?|as)\\s+)(?:(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*)(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "class-name", + compileHighlightPattern( + "\\b(?:(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?)(?=\\s+(?!(?:\\b(?:bool|byte|char|decimal|double|dynamic|float|int|long|object|sbyte|short|string|uint|ulong|ushort|var|void|class|enum|interface|record|struct|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b)|with\\s*\\{)(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*[=,;:{)\\]]|\\s+(?:in|when)\\b))"), + inside: () => _g8), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:bool|byte|char|decimal|double|dynamic|float|int|long|object|sbyte|short|string|uint|ulong|ushort|var|void|class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken("range", compileHighlightPattern("\\.\\."), alias: "operator"), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0(?:x[\\da-f_]*[\\da-f]|b[01_]*[01])|(?:\\B\\.\\d+(?:_+\\d+)*|\\b\\d+(?:_+\\d+)*(?:\\.\\d+(?:_+\\d+)*)?)(?:e[-+]?\\d+(?:_+\\d+)*)?)(?:[dflmu]|lu|ul)?\\b", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + ">>=?|<<=?|[-=]>|([-+&|])\\1|~|\\?\\?=?|[-+*/%&|^!=<>]=?")), + GrammarToken("named-parameter", + compileHighlightPattern("([(,]\\s*)(?:@?\\b[A-Za-z_]\\w*\\b)(?=\\s*:)"), + lookbehind: true, alias: "punctuation"), + GrammarToken( + "punctuation", compileHighlightPattern("\\?\\.?|::|[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^{])(?:\\{\\{)*)(?:\\{(?!\\{)(?:(?![}:])(?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\([^\\s\\S]*\\))*\\))*\\))*\\)))*(?::[^}\\r\\n]+)?\\})"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "format-string", + compileHighlightPattern( + "(^\\{(?:(?![}:])(?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\([^\\s\\S]*\\))*\\))*\\))*\\)))*)(?::[^}\\r\\n]+)(?=\\}\$)"), + lookbehind: true, + inside: () => _g3), + GrammarToken("punctuation", compileHighlightPattern("^\\{|\\}\$")), + GrammarToken("expression", compileHighlightPattern("[\\s\\S]+"), + alias: "language-csharp", inside: () => _g0), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^:")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^{])(?:\\{\\{)*)(?:\\{(?!\\{)(?:(?![}:])(?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\([^\\s\\S]*\\))*\\))*\\))*\\)))*(?::[^}\\r\\n]+)?\\})"), + lookbehind: true, + inside: () => _g5), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "format-string", + compileHighlightPattern( + "(^\\{(?:(?![}:])(?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\((?:[^\"'/()]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})')|\\([^\\s\\S]*\\))*\\))*\\))*\\)))*)(?::[^}\\r\\n]+)(?=\\}\$)"), + lookbehind: true, + inside: () => _g6), + GrammarToken("punctuation", compileHighlightPattern("^\\{|\\}\$")), + GrammarToken("expression", compileHighlightPattern("[\\s\\S]+"), + alias: "language-csharp", inside: () => _g0), +]); + +final Grammar _g6 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^:")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g8 = Grammar([ + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:bool|byte|char|decimal|double|dynamic|float|int|long|object|sbyte|short|string|uint|ulong|ushort|var|void|class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[<>()?,.:[\\]]")), +]); + +final Grammar _g9 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^(?:@?\\b[A-Za-z_]\\w*\\b)")), + GrammarToken( + "generic", + compileHighlightPattern( + "<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>"), + alias: "class-name", + inside: () => _g8), +]); + +final Grammar _g10 = Grammar([ + GrammarToken( + "record-arguments", + compileHighlightPattern( + "(^(?!new\\s*\\()(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)\\s*)(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))"), + lookbehind: true, + greedy: true, + inside: () => _g0), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:bool|byte|char|decimal|double|dynamic|float|int|long|object|sbyte|short|string|uint|ulong|ushort|var|void|class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b")), + GrammarToken( + "class-name", + compileHighlightPattern( + "(?:(?:\\((?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+(?:,(?:[^,()<>[\\];=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>)|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|(?:\\((?:[^()]|[^\\s\\S])*\\)))*\\)))*\\)))*\\))|(?:\\[\\s*(?:,\\s*)*\\]))+)+\\))|(?:(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*))(?:\\s*(?:\\?\\s*)?(?:\\[\\s*(?:,\\s*)*\\]))*(?:\\s*\\?)?"), + greedy: true, + inside: () => _g8), + GrammarToken("punctuation", compileHighlightPattern("[,()]")), +]); + +final Grammar _g11 = Grammar([ + GrammarToken( + "directive", + compileHighlightPattern( + "(#)\\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\\b"), + lookbehind: true, + alias: "keyword"), +]); + +final Grammar _g12 = Grammar([ + GrammarToken( + "target", + compileHighlightPattern( + "^(?:\\b(?:assembly|event|field|method|module|param|property|return|type)\\b)(?=\\s*:)"), + alias: "keyword"), + GrammarToken( + "attribute-arguments", + compileHighlightPattern( + "\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\((?:[^\"'/()]|(?:\\/(?![*/])|\\/\\/[^\\r\\n]*[\\r\\n]|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|(?:\"(?:\\\\.|[^\\\\\"\\r\\n])*\"|'(?:[^\\r\\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'))|\\([^\\s\\S]*\\))*\\))*\\))*\\))*\\)"), + inside: () => _g0), + GrammarToken( + "class-name", + compileHighlightPattern( + "(?!(?:\\b(?:class|enum|interface|record|struct|add|alias|and|ascending|async|await|by|descending|from(?=\\s*(?:\\w|\$))|get|global|group|into|init(?=\\s*;)|join|let|nameof|not|notnull|on|or|orderby|partial|remove|select|set|unmanaged|value|when|where|with(?=\\s*{)|abstract|as|base|break|case|catch|checked|const|continue|default|delegate|do|else|event|explicit|extern|finally|fixed|for|foreach|goto|if|implicit|in|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|switch|this|throw|try|typeof|unchecked|unsafe|using|virtual|volatile|while|yield)\\b))(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?)(?:\\s*\\.\\s*(?:(?:@?\\b[A-Za-z_]\\w*\\b)(?:\\s*(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|(?:<(?:[^<>;=+\\-*/%&|^]|[^\\s\\S])*>))*>))*>))*>))?))*"), + inside: () => _g13), + GrammarToken("punctuation", compileHighlightPattern("[:,]")), +]); + +final Grammar _g13 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/css.dart b/lib/highlight/css.dart new file mode 100644 index 0000000..7ab295f --- /dev/null +++ b/lib/highlight/css.dart @@ -0,0 +1,78 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `css`. +/// +/// Import this library only when you need `css` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightCss { + /// The grammar for `css`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/")), + GrammarToken( + "atrule", + compileHighlightPattern( + "@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*?(?:;|(?=\\s*\\{))"), + inside: () => _g1), + GrammarToken( + "url", + compileHighlightPattern( + "\\burl\\((?:(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')|(?:[^\\\\\\r\\n()\"']|\\\\[\\s\\S])*)\\)", + caseSensitive: false), + greedy: true, + inside: () => _g2), + GrammarToken( + "selector", + compileHighlightPattern( + "(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*(?=\\s*\\{)"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken( + "property", + compileHighlightPattern( + "(^|[^-\\w\\xA0-\\uFFFF])(?!\\s)[-_a-z\\xA0-\\uFFFF](?:(?!\\s)[-\\w\\xA0-\\uFFFF])*(?=\\s*:)", + caseSensitive: false), + lookbehind: true), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("rule", compileHighlightPattern("^@[\\w-]+")), + GrammarToken( + "selector-function-argument", + compileHighlightPattern( + "(\\bselector\\s*\\(\\s*(?![\\s)]))(?:[^()\\s]|\\s+(?![\\s)])|\\((?:[^()]|\\([^()]*\\))*\\))+(?=\\s*\\))"), + lookbehind: true, + alias: "selector"), + GrammarToken("keyword", + compileHighlightPattern("(^|[^\\w-])(?:and|not|only|or)(?![\\w-])"), + lookbehind: true), +], rest: () => _g0); + +final Grammar _g2 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^url", caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("^\\(|\\)\$")), + GrammarToken( + "string", + compileHighlightPattern( + "^(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')\$"), + alias: "url"), +]); diff --git a/lib/highlight/dart.dart b/lib/highlight/dart.dart new file mode 100644 index 0000000..29a15b6 --- /dev/null +++ b/lib/highlight/dart.dart @@ -0,0 +1,109 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `dart`. +/// +/// Import this library only when you need `dart` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightDart { + /// The grammar for `dart`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string-literal", + compileHighlightPattern( + "r?(?:(\"\"\"|''')[\\s\\S]*?\\1|([\"'])(?:\\\\.|(?!\\2)[^\\\\\\r\\n])*\\2(?!\\2))"), + greedy: true, + inside: () => _g1), + GrammarToken("metadata", compileHighlightPattern("@\\w+"), alias: "function"), + GrammarToken( + "generics", + compileHighlightPattern( + "<(?:[\\w\\s,.&?]|<(?:[\\w\\s,.&?]|<(?:[\\w\\s,.&?]|<[\\w\\s,.&?]*>)*>)*>)*>"), + inside: () => _g3), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z]\\w*(?=\\s+\\w+\\s*[;,=()])"), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "keyword", compileHighlightPattern("\\b(?:async|sync|yield)\\*")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\da-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "\\bis!|\\b(?:as|is)\\b|\\+\\+|--|&&|\\|\\||<<=?|>>=?|~(?:\\/=?)?|[+\\-*\\/%&^|=!<>]=?|\\?")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$(?:\\w+|\\{(?:[^{}]|\\{[^{}]*\\})*\\})"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^\\\$\\{?|\\}\$")), + GrammarToken("expression", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g0), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "keyword", compileHighlightPattern("\\b(?:async|sync|yield)\\*")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[<>(),.:]")), + GrammarToken("operator", compileHighlightPattern("[?&|]")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g5), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/diff.dart b/lib/highlight/diff.dart new file mode 100644 index 0000000..03fe885 --- /dev/null +++ b/lib/highlight/diff.dart @@ -0,0 +1,101 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `diff`. +/// +/// Import this library only when you need `diff` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightDiff { + /// The grammar for `diff`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("coord", + compileHighlightPattern("^(?:\\*{3}|-{3}|\\+{3}).*\$", multiLine: true)), + GrammarToken("coord", compileHighlightPattern("^@@.*@@\$", multiLine: true)), + GrammarToken("coord", compileHighlightPattern("^\\d.*\$", multiLine: true)), + GrammarToken( + "deleted-sign", + compileHighlightPattern("^(?:[-].*(?:\\r\\n?|\\n|(?![\\s\\S])))+", + multiLine: true), + alias: "deleted", + inside: () => _g1), + GrammarToken( + "deleted-arrow", + compileHighlightPattern("^(?:[<].*(?:\\r\\n?|\\n|(?![\\s\\S])))+", + multiLine: true), + alias: "deleted", + inside: () => _g2), + GrammarToken( + "inserted-sign", + compileHighlightPattern("^(?:[+].*(?:\\r\\n?|\\n|(?![\\s\\S])))+", + multiLine: true), + alias: "inserted", + inside: () => _g3), + GrammarToken( + "inserted-arrow", + compileHighlightPattern("^(?:[>].*(?:\\r\\n?|\\n|(?![\\s\\S])))+", + multiLine: true), + alias: "inserted", + inside: () => _g4), + GrammarToken( + "unchanged", + compileHighlightPattern("^(?:[ ].*(?:\\r\\n?|\\n|(?![\\s\\S])))+", + multiLine: true), + inside: () => _g5), + GrammarToken( + "diff", + compileHighlightPattern("^(?:[!].*(?:\\r\\n?|\\n|(?![\\s\\S])))+", + multiLine: true), + alias: "bold", + inside: () => _g6), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "line", compileHighlightPattern("(.)(?=[\\s\\S]).*(?:\\r\\n?|\\n)?"), + lookbehind: true), + GrammarToken("prefix", compileHighlightPattern("[\\s\\S]"), alias: "deleted"), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "line", compileHighlightPattern("(.)(?=[\\s\\S]).*(?:\\r\\n?|\\n)?"), + lookbehind: true), + GrammarToken("prefix", compileHighlightPattern("[\\s\\S]"), alias: "deleted"), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "line", compileHighlightPattern("(.)(?=[\\s\\S]).*(?:\\r\\n?|\\n)?"), + lookbehind: true), + GrammarToken("prefix", compileHighlightPattern("[\\s\\S]"), + alias: "inserted"), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "line", compileHighlightPattern("(.)(?=[\\s\\S]).*(?:\\r\\n?|\\n)?"), + lookbehind: true), + GrammarToken("prefix", compileHighlightPattern("[\\s\\S]"), + alias: "inserted"), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "line", compileHighlightPattern("(.)(?=[\\s\\S]).*(?:\\r\\n?|\\n)?"), + lookbehind: true), + GrammarToken("prefix", compileHighlightPattern("[\\s\\S]"), + alias: "unchanged"), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "line", compileHighlightPattern("(.)(?=[\\s\\S]).*(?:\\r\\n?|\\n)?"), + lookbehind: true), + GrammarToken("prefix", compileHighlightPattern("[\\s\\S]"), alias: "diff"), +]); diff --git a/lib/highlight/docker.dart b/lib/highlight/docker.dart new file mode 100644 index 0000000..a6d9e32 --- /dev/null +++ b/lib/highlight/docker.dart @@ -0,0 +1,88 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `docker`. +/// +/// Import this library only when you need `docker` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightDocker { + /// The grammar for `docker`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "instruction", + compileHighlightPattern( + "(^[ \\t]*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\\s)(?:\\\\.|[^\\r\\n\\\\])*(?:\\\\\$(?:\\s|#.*\$)*(?![\\s#])(?:\\\\.|[^\\r\\n\\\\])*)*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + greedy: true, + inside: () => _g1), + GrammarToken( + "comment", compileHighlightPattern("(^[ \\t]*)#.*", multiLine: true), + lookbehind: true, greedy: true), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "options", + compileHighlightPattern( + "(^(?:ONBUILD(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))?\\w+(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))--[\\w-]+=(?:\"(?:[^\"\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\"|'(?:[^'\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*'|(?![\"'])(?:[^\\s\\\\]|\\\\.)+)(?:(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))--[\\w-]+=(?:\"(?:[^\"\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\"|'(?:[^'\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*'|(?![\"'])(?:[^\\s\\\\]|\\\\.)+))*", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^(?:ONBUILD(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))?HEALTHCHECK(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))(?:--[\\w-]+=(?:\"(?:[^\"\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\"|'(?:[^'\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*'|(?![\"'])(?:[^\\s\\\\]|\\\\.)+)(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))*)(?:CMD|NONE)\\b", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^(?:ONBUILD(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))?FROM(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))(?:--[\\w-]+=(?:\"(?:[^\"\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\"|'(?:[^'\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*'|(?![\"'])(?:[^\\s\\\\]|\\\\.)+)(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))*(?!--)[^ \\t\\\\]+(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))AS", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^ONBUILD(?:[ \\t]+(?![ \\t])(?:\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n]))?|\\\\[\\r\\n](?:\\s|\\\\[\\r\\n]|#.*(?!.))*(?![\\s#]|\\\\[\\r\\n])))\\w+", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken("keyword", compileHighlightPattern("^\\w+"), greedy: true), + GrammarToken( + "comment", compileHighlightPattern("(^[ \\t]*)#.*", multiLine: true), + lookbehind: true, greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:[^\"\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\"|'(?:[^'\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*'"), + greedy: true), + GrammarToken( + "variable", compileHighlightPattern("\\\$(?:\\w+|\\{[^{}\"'\\\\]*\\})")), + GrammarToken("operator", compileHighlightPattern("\\\\\$", multiLine: true)), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("property", compileHighlightPattern("(^|\\s)--[\\w-]+"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:[^\"\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\"|'(?:[^'\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*'"), + greedy: true), + GrammarToken( + "string", compileHighlightPattern("(=)(?![\"'])(?:[^\\s\\\\]|\\\\.)+"), + lookbehind: true), + GrammarToken("operator", compileHighlightPattern("\\\\\$", multiLine: true)), + GrammarToken("punctuation", compileHighlightPattern("=")), +]); diff --git a/lib/highlight/elixir.dart b/lib/highlight/elixir.dart new file mode 100644 index 0000000..65d1fd9 --- /dev/null +++ b/lib/highlight/elixir.dart @@ -0,0 +1,109 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `elixir`. +/// +/// Import this library only when you need `elixir` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightElixir { + /// The grammar for `elixir`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "doc", + compileHighlightPattern( + "@(?:doc|moduledoc)\\s+(?:(\"\"\"|''')[\\s\\S]*?\\1|(\"|')(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2)"), + inside: () => _g1), + GrammarToken("comment", compileHighlightPattern("#.*"), greedy: true), + GrammarToken( + "regex", + compileHighlightPattern( + "~[rR](?:(\"\"\"|''')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])+\\1|([\\/|\"'])(?:\\\\.|(?!\\2)[^\\\\\\r\\n])+\\2|\\((?:\\\\.|[^\\\\)\\r\\n])+\\)|\\[(?:\\\\.|[^\\\\\\]\\r\\n])+\\]|\\{(?:\\\\.|[^\\\\}\\r\\n])+\\}|<(?:\\\\.|[^\\\\>\\r\\n])+>)[uismxfr]*"), + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "~[cCsSwW](?:(\"\"\"|''')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])+\\1|([\\/|\"'])(?:\\\\.|(?!\\2)[^\\\\\\r\\n])+\\2|\\((?:\\\\.|[^\\\\)\\r\\n])+\\)|\\[(?:\\\\.|[^\\\\\\]\\r\\n])+\\]|\\{(?:\\\\.|#\\{[^}]+\\}|#(?!\\{)|[^#\\\\}\\r\\n])+\\}|<(?:\\\\.|[^\\\\>\\r\\n])+>)[csa]?"), + greedy: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("(\"\"\"|''')[\\s\\S]*?\\1"), + greedy: true, inside: () => _g4), + GrammarToken( + "string", + compileHighlightPattern( + "(\"|')(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true, + inside: () => _g6), + GrammarToken("atom", compileHighlightPattern("(^|[^:]):\\w+"), + lookbehind: true, alias: "symbol"), + GrammarToken("module", compileHighlightPattern("\\b[A-Z]\\w*\\b"), + alias: "class-name"), + GrammarToken("attr-name", compileHighlightPattern("\\b\\w+\\??:(?!:)")), + GrammarToken("argument", compileHighlightPattern("(^|[^&])&\\d+"), + lookbehind: true, alias: "variable"), + GrammarToken("attribute", compileHighlightPattern("@\\w+"), + alias: "variable"), + GrammarToken( + "function", + compileHighlightPattern( + "\\b[_a-zA-Z]\\w*[?!]?(?:(?=\\s*(?:\\.\\s*)?\\()|(?=\\/\\d))")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:0[box][a-f\\d_]+|\\d[\\d_]*)(?:\\.[\\d_]+)?(?:e[+-]?[\\d_]+)?\\b", + caseSensitive: false)), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:after|alias|and|case|catch|cond|def(?:callback|delegate|exception|impl|macro|module|n|np|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|quote|raise|require|rescue|try|unless|unquote|use|when)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|nil|true)\\b")), + GrammarToken( + "operator", + compileHighlightPattern( + "\\bin\\b|&&?|\\|[|>]?|\\\\\\\\|::|\\.\\.\\.?|\\+\\+?|-[->]?|<[-=>]|>=|!==?|\\B!|=(?:==?|[>~])?|[*\\/^]")), + GrammarToken("operator", compileHighlightPattern("([^<])<(?!<)"), + lookbehind: true), + GrammarToken("operator", compileHighlightPattern("([^>])>(?!>)"), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("<<|>>|[.,%\\[\\]{}()]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("attribute", compileHighlightPattern("^@\\w+")), + GrammarToken("string", compileHighlightPattern("['\"][\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("interpolation", compileHighlightPattern("#\\{[^}]+\\}"), + inside: () => _g3), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("delimiter", compileHighlightPattern("^#\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g4 = Grammar([ + GrammarToken("interpolation", compileHighlightPattern("#\\{[^}]+\\}"), + inside: () => _g5), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("delimiter", compileHighlightPattern("^#\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g6 = Grammar([ + GrammarToken("interpolation", compileHighlightPattern("#\\{[^}]+\\}"), + inside: () => _g7), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("delimiter", compileHighlightPattern("^#\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g0); diff --git a/lib/highlight/elm.dart b/lib/highlight/elm.dart new file mode 100644 index 0000000..9a9df9a --- /dev/null +++ b/lib/highlight/elm.dart @@ -0,0 +1,62 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `elm`. +/// +/// Import this library only when you need `elm` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightElm { + /// The grammar for `elm`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("--.*|\\{-[\\s\\S]*?-\\}")), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:[^\\\\'\\r\\n]|\\\\(?:[abfnrtv\\\\']|\\d+|x[0-9a-fA-F]+|u\\{[0-9a-fA-F]+\\}))'"), + greedy: true), + GrammarToken("string", compileHighlightPattern("\"\"\"[\\s\\S]*?\"\"\""), + greedy: true), + GrammarToken( + "string", compileHighlightPattern("\"(?:[^\\\\\"\\r\\n]|\\\\.)*\""), + greedy: true), + GrammarToken( + "import-statement", + compileHighlightPattern( + "(^[\\t ]*)import\\s+[A-Z]\\w*(?:\\.[A-Z]\\w*)*(?:\\s+as\\s+(?:[A-Z]\\w*)(?:\\.[A-Z]\\w*)*)?(?:\\s+exposing\\s+)?", + multiLine: true), + lookbehind: true, + inside: () => _g1), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:alias|as|case|else|exposing|if|in|infixl|infixr|let|module|of|then|type)\\b")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:abs|acos|always|asin|atan|atan2|ceiling|clamp|compare|cos|curry|degrees|e|flip|floor|fromPolar|identity|isInfinite|isNaN|logBase|max|min|negate|never|not|pi|radians|rem|round|sin|sqrt|tan|toFloat|toPolar|toString|truncate|turns|uncurry|xor)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?|0x[0-9a-f]+)\\b", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "\\s\\.\\s|[+\\-/*=.\$<>:&|^?%#@~!]{2,}|[+\\-/*=\$<>:&|^?%#@~!]")), + GrammarToken( + "hvariable", compileHighlightPattern("\\b(?:[A-Z]\\w*\\.)*[a-z]\\w*\\b")), + GrammarToken( + "constant", compileHighlightPattern("\\b(?:[A-Z]\\w*\\.)*[A-Z]\\w*\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\]|(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "keyword", compileHighlightPattern("\\b(?:as|exposing|import)\\b")), +]); diff --git a/lib/highlight/erlang.dart b/lib/highlight/erlang.dart new file mode 100644 index 0000000..7564541 --- /dev/null +++ b/lib/highlight/erlang.dart @@ -0,0 +1,55 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `erlang`. +/// +/// Import this library only when you need `erlang` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightErlang { + /// The grammar for `erlang`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("%.+")), + GrammarToken( + "string", compileHighlightPattern("\"(?:\\\\.|[^\\\\\"\\r\\n])*\""), + greedy: true), + GrammarToken("quoted-function", + compileHighlightPattern("'(?:\\\\.|[^\\\\'\\r\\n])+'(?=\\()"), + alias: "function"), + GrammarToken( + "quoted-atom", compileHighlightPattern("'(?:\\\\.|[^\\\\'\\r\\n])+'"), + alias: "atom"), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:after|begin|case|catch|end|fun|if|of|receive|try|when)\\b")), + GrammarToken("number", compileHighlightPattern("\\\$\\\\?.")), + GrammarToken("number", + compileHighlightPattern("\\b\\d+#[a-z0-9]+", caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken("function", compileHighlightPattern("\\b[a-z][\\w@]*(?=\\()")), + GrammarToken( + "variable", compileHighlightPattern("(^|[^@])(?:\\b|\\?)[A-Z_][\\w@]*"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "[=\\/<>:]=|=[:\\/]=|\\+\\+?|--?|[=*\\/!]|\\b(?:and|andalso|band|bnot|bor|bsl|bsr|bxor|div|not|or|orelse|rem|xor)\\b")), + GrammarToken("operator", compileHighlightPattern("(^|[^<])<(?!<)"), + lookbehind: true), + GrammarToken("operator", compileHighlightPattern("(^|[^>])>(?!>)"), + lookbehind: true), + GrammarToken("atom", compileHighlightPattern("\\b[a-z][\\w@]*")), + GrammarToken( + "punctuation", compileHighlightPattern("[()[\\]{}:;,.#|]|<<|>>")), +]); diff --git a/lib/highlight/fsharp.dart b/lib/highlight/fsharp.dart new file mode 100644 index 0000000..33cb30e --- /dev/null +++ b/lib/highlight/fsharp.dart @@ -0,0 +1,87 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `fsharp`. +/// +/// Import this library only when you need `fsharp` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightFsharp { + /// The grammar for `fsharp`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\(\\*(?!\\))[\\s\\S]*?\\*\\)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("annotation", compileHighlightPattern("\\[<.+?>\\]"), + greedy: true, inside: () => _g1), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:[^\\\\']|\\\\(?:.|\\d{3}|x[a-fA-F\\d]{2}|u[a-fA-F\\d]{4}|U[a-fA-F\\d]{8}))'B?"), + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"\"\"[\\s\\S]*?\"\"\"|@\"(?:\"\"|[^\"])*\"|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")B?"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:exception|inherit|interface|new|of|type)\\s+|\\w\\s*:\\s*|\\s:\\??>\\s*)[.\\w]+\\b(?:\\s*(?:->|\\*)\\s*[.\\w]+\\b)*(?!\\s*[:.])"), + lookbehind: true, + inside: () => _g2), + GrammarToken( + "preprocessor", compileHighlightPattern("(^[\\t ]*)#.*", multiLine: true), + lookbehind: true, alias: "property", inside: () => _g3), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:let|return|use|yield)(?:!\\B|\\b)|\\b(?:abstract|and|as|asr|assert|atomic|base|begin|break|checked|class|component|const|constraint|constructor|continue|default|delegate|do|done|downcast|downto|eager|elif|else|end|event|exception|extern|external|false|finally|fixed|for|fun|function|functor|global|if|in|include|inherit|inline|interface|internal|land|lazy|lor|lsl|lsr|lxor|match|member|method|mixin|mod|module|mutable|namespace|new|not|null|object|of|open|or|override|parallel|private|process|protected|public|pure|rec|sealed|select|sig|static|struct|tailcall|then|to|trait|true|try|type|upcast|val|virtual|void|volatile|when|while|with)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "number", compileHighlightPattern("\\b0x[\\da-fA-F]+(?:LF|lf|un)?\\b")), + GrammarToken("number", compileHighlightPattern("\\b0b[01]+(?:uy|y)?\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[fm]|e[+-]?\\d+)?\\b", + caseSensitive: false)), + GrammarToken( + "number", compileHighlightPattern("\\b\\d+(?:[IlLsy]|UL|u[lsy]?)?\\b")), + GrammarToken( + "operator", + compileHighlightPattern( + "([<>~&^])\\1\\1|([*.:<>&])\\2|<-|->|[!=:]=|?|\\??(?:<=|>=|<>|[-+*/%=<>])\\??|[!?^&]|~[+~-]|:>|:\\?>?")), + GrammarToken("computation-expression", + compileHighlightPattern("\\b[_a-z]\\w*(?=\\s*\\{)", caseSensitive: false), + alias: "keyword"), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^\\[<|>\\]\$")), + GrammarToken("class-name", + compileHighlightPattern("^\\w+\$|(^|;\\s*)[A-Z]\\w*(?=\\()"), + lookbehind: true), + GrammarToken("annotation-content", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g0), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("operator", compileHighlightPattern("->|\\*")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("directive", + compileHighlightPattern("(^#)\\b(?:else|endif|if|light|line|nowarn)\\b"), + lookbehind: true, alias: "keyword"), +]); diff --git a/lib/highlight/git.dart b/lib/highlight/git.dart new file mode 100644 index 0000000..a016572 --- /dev/null +++ b/lib/highlight/git.dart @@ -0,0 +1,32 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `git`. +/// +/// Import this library only when you need `git` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightGit { + /// The grammar for `git`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("^#.*", multiLine: true)), + GrammarToken("deleted", compileHighlightPattern("^[-โ€“].*", multiLine: true)), + GrammarToken("inserted", compileHighlightPattern("^\\+.*", multiLine: true)), + GrammarToken("string", + compileHighlightPattern("(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1")), + GrammarToken( + "command", compileHighlightPattern("^.*\\\$ git .*\$", multiLine: true), + inside: () => _g1), + GrammarToken("coord", compileHighlightPattern("^@@.*@@\$", multiLine: true)), + GrammarToken("commit-sha1", + compileHighlightPattern("^commit \\w{40}\$", multiLine: true)), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("parameter", compileHighlightPattern("\\s--?\\w+")), +]); diff --git a/lib/highlight/go.dart b/lib/highlight/go.dart new file mode 100644 index 0000000..b34962e --- /dev/null +++ b/lib/highlight/go.dart @@ -0,0 +1,61 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `go`. +/// +/// Import this library only when you need `go` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightGo { + /// The grammar for `go`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "char", compileHighlightPattern("'(?:\\\\.|[^'\\\\\\r\\n]){0,10}'"), + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^\\\\])\"(?:\\\\.|[^\"\\\\\\r\\n])*\"|`[^`]*`"), + lookbehind: true, + greedy: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\\b")), + GrammarToken( + "boolean", compileHighlightPattern("\\b(?:_|false|iota|nil|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "number", + compileHighlightPattern("\\b0(?:b[01_]+|o[0-7_]+)i?\\b", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x(?:[a-f\\d_]+(?:\\.[a-f\\d_]*)?|\\.[a-f\\d_]+)(?:p[+-]?\\d+(?:_\\d+)*)?i?(?!\\w)", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b\\d[\\d_]*(?:\\.[\\d_]*)?|\\B\\.\\d[\\d_]*)(?:e[+-]?[\\d_]+)?i?(?!\\w)", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "[*\\/%^!=]=?|\\+[=+]?|-[=-]?|\\|[=|]?|&(?:=|&|\\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\\.\\.\\.")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\\b")), +]); diff --git a/lib/highlight/graphql.dart b/lib/highlight/graphql.dart new file mode 100644 index 0000000..ddd92e8 --- /dev/null +++ b/lib/highlight/graphql.dart @@ -0,0 +1,85 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'markdown.dart'; + +/// Syntax grammar for `graphql`. +/// +/// Import this library only when you need `graphql` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightGraphql { + /// The grammar for `graphql`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("#.*")), + GrammarToken( + "description", + compileHighlightPattern( + "(?:\"\"\"(?:[^\"]|(?!\"\"\")\")*\"\"\"|\"(?:\\\\.|[^\\\\\"\\r\\n])*\")(?=\\s*[a-z_])", + caseSensitive: false), + greedy: true, + alias: "string", + inside: () => _g1), + GrammarToken( + "string", + compileHighlightPattern( + "\"\"\"(?:[^\"]|(?!\"\"\")\")*\"\"\"|\"(?:\\\\.|[^\\\\\"\\r\\n])*\""), + greedy: true), + GrammarToken( + "number", + compileHighlightPattern("(?:\\B-|\\b)\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b", + caseSensitive: false)), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("variable", + compileHighlightPattern("\\\$[a-z_]\\w*", caseSensitive: false)), + GrammarToken( + "directive", compileHighlightPattern("@[a-z_]\\w*", caseSensitive: false), + alias: "function"), + GrammarToken( + "attr-name", + compileHighlightPattern( + "\\b[a-z_]\\w*(?=\\s*(?:\\((?:[^()\"]|\"(?:\\\\.|[^\\\\\"\\r\\n])*\")*\\))?:)", + caseSensitive: false), + greedy: true), + GrammarToken("atom-input", compileHighlightPattern("\\b[A-Z]\\w*Input\\b"), + alias: "class-name"), + GrammarToken("scalar", + compileHighlightPattern("\\b(?:Boolean|Float|ID|Int|String)\\b")), + GrammarToken("constant", compileHighlightPattern("\\b[A-Z][A-Z_\\d]*\\b")), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:enum|implements|interface|on|scalar|type|union)\\s+|&\\s*|:\\s*|\\[)[A-Z_]\\w*"), + lookbehind: true), + GrammarToken( + "fragment", + compileHighlightPattern( + "(\\bfragment\\s+|\\.{3}\\s*(?!on\\b))[a-zA-Z_]\\w*"), + lookbehind: true, + alias: "function"), + GrammarToken("definition-mutation", + compileHighlightPattern("(\\bmutation\\s+)[a-zA-Z_]\\w*"), + lookbehind: true, alias: "function"), + GrammarToken("definition-query", + compileHighlightPattern("(\\bquery\\s+)[a-zA-Z_]\\w*"), + lookbehind: true, alias: "function"), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\\b")), + GrammarToken("operator", compileHighlightPattern("[!=|&]|\\.{3}")), + GrammarToken("property-query", compileHighlightPattern("\\w+(?=\\s*\\()")), + GrammarToken("object", compileHighlightPattern("\\w+(?=\\s*\\{)")), + GrammarToken("punctuation", compileHighlightPattern("[!(){}\\[\\]:=,]")), + GrammarToken("property", compileHighlightPattern("\\w+")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("language-markdown", + compileHighlightPattern("(^\"(?:\"\")?)(?!\\1)[\\s\\S]+(?=\\1\$)"), + lookbehind: true, inside: () => HighlightMarkdown.grammar), +]); diff --git a/lib/highlight/groovy.dart b/lib/highlight/groovy.dart new file mode 100644 index 0000000..ffed684 --- /dev/null +++ b/lib/highlight/groovy.dart @@ -0,0 +1,86 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `groovy`. +/// +/// Import this library only when you need `groovy` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightGroovy { + /// The grammar for `groovy`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("shebang", compileHighlightPattern("#!.+"), + greedy: true, alias: "comment"), + GrammarToken( + "interpolation-string", + compileHighlightPattern( + "\"\"\"(?:[^\\\\]|\\\\[\\s\\S])*?\"\"\"|([\"/])(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1|\\\$\\/(?:[^/\$]|\\\$(?:[/\$]|(?![/\$]))|\\/(?!\\\$))*\\/\\\$"), + greedy: true, + inside: () => _g1), + GrammarToken( + "string", + compileHighlightPattern( + "'''(?:[^\\\\]|\\\\[\\s\\S])*?'''|'(?:\\\\.|[^\\\\'\\r\\n])*'"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g3), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|as|assert|boolean|break|byte|case|catch|char|class|const|continue|def|default|do|double|else|enum|extends|final|finally|float|for|goto|if|implements|import|in|instanceof|int|interface|long|native|new|package|private|protected|public|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|trait|transient|try|void|volatile|while)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("annotation", compileHighlightPattern("(^|[^.])@\\w+"), + lookbehind: true, alias: "punctuation"), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:0b[01_]+|0x[\\da-f_]+(?:\\.[\\da-f_p\\-]+)?|[\\d_]+(?:\\.[\\d_]+)?(?:e[+-]?\\d+)?)[glidf]?\\b", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "(^|[^.])(?:~|==?~?|\\?[.:]?|\\*(?:[.=]|\\*=?)?|\\.[@&]|\\.\\.<|\\.\\.(?!\\.)|-[-=>]?|\\+[+=]?|!=?|<(?:<=?|=>?)?|>(?:>>?=?|=)?|&[&=]?|\\|[|=]?|\\/=?|\\^=?|%=?)"), + lookbehind: true), + GrammarToken( + "spock-block", + compileHighlightPattern( + "\\b(?:and|cleanup|expect|given|setup|then|when|where):")), + GrammarToken("punctuation", compileHighlightPattern("\\.+|[{}[\\];(),:\$]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\\$])(?:\\\\{2})*)\\\$(?:\\w+|\\{[^{}]*\\})"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{?|\\}\$"), + alias: "punctuation"), + GrammarToken("expression", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g0), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); diff --git a/lib/highlight/handlebars.dart b/lib/highlight/handlebars.dart new file mode 100644 index 0000000..aa2e25a --- /dev/null +++ b/lib/highlight/handlebars.dart @@ -0,0 +1,44 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `handlebars`. +/// +/// Import this library only when you need `handlebars` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightHandlebars { + /// The grammar for `handlebars`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\{\\{![\\s\\S]*?\\}\\}")), + GrammarToken("delimiter", compileHighlightPattern("^\\{\\{\\{?|\\}\\}\\}?\$"), + alias: "punctuation"), + GrammarToken("string", + compileHighlightPattern("([\"'])(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\dA-Fa-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[Ee][+-]?\\d+)?")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "block", + compileHighlightPattern( + "^(\\s*(?:~\\s*)?)[#\\/]\\S+?(?=\\s*(?:~\\s*)?\$|\\s)"), + lookbehind: true, + alias: "keyword"), + GrammarToken("brackets", compileHighlightPattern("\\[[^\\]]+\\]"), + inside: () => _g1), + GrammarToken("punctuation", + compileHighlightPattern("[!\"#%&':()*+,.\\/;<=>@\\[\\\\\\]^`{|}~]")), + GrammarToken("variable", + compileHighlightPattern("[^!\"#%&'()*+,\\/;<=>@\\[\\\\\\]^`{|}~\\s]+")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\[|\\]")), + GrammarToken("variable", compileHighlightPattern("[\\s\\S]+")), +]); diff --git a/lib/highlight/haskell.dart b/lib/highlight/haskell.dart new file mode 100644 index 0000000..f37693b --- /dev/null +++ b/lib/highlight/haskell.dart @@ -0,0 +1,81 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `haskell`. +/// +/// Import this library only when you need `haskell` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightHaskell { + /// The grammar for `haskell`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "(^|[^-!#\$%*+=?&@|~.:<>^\\\\\\/])(?:--(?:(?=.)[^-!#\$%*+=?&@|~.:<>^\\\\\\/].*|\$)|\\{-[\\s\\S]*?-\\})", + multiLine: true), + lookbehind: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:[^\\\\']|\\\\(?:[abfnrtv\\\\\"'&]|\\^[A-Z@[\\]^_]|ACK|BEL|BS|CAN|CR|DC1|DC2|DC3|DC4|DEL|DLE|EM|ENQ|EOT|ESC|ETB|ETX|FF|FS|GS|HT|LF|NAK|NUL|RS|SI|SO|SOH|SP|STX|SUB|SYN|US|VT|\\d+|o[0-7]+|x[0-9a-fA-F]+))'"), + alias: "string"), + GrammarToken("string", + compileHighlightPattern("\"(?:[^\\\\\"]|\\\\(?:\\S|\\s+\\\\))*\""), + greedy: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:case|class|data|deriving|do|else|if|in|infixl|infixr|instance|let|module|newtype|of|primitive|then|type|where)\\b")), + GrammarToken( + "import-statement", + compileHighlightPattern( + "(^[\\t ]*)import\\s+(?:qualified\\s+)?(?:[A-Z][\\w']*)(?:\\.[A-Z][\\w']*)*(?:\\s+as\\s+(?:[A-Z][\\w']*)(?:\\.[A-Z][\\w']*)*)?(?:\\s+hiding\\b)?", + multiLine: true), + lookbehind: true, + inside: () => _g1), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:abs|acos|acosh|all|and|any|appendFile|approxRational|asTypeOf|asin|asinh|atan|atan2|atanh|basicIORun|break|catch|ceiling|chr|compare|concat|concatMap|const|cos|cosh|curry|cycle|decodeFloat|denominator|digitToInt|div|divMod|drop|dropWhile|either|elem|encodeFloat|enumFrom|enumFromThen|enumFromThenTo|enumFromTo|error|even|exp|exponent|fail|filter|flip|floatDigits|floatRadix|floatRange|floor|fmap|foldl|foldl1|foldr|foldr1|fromDouble|fromEnum|fromInt|fromInteger|fromIntegral|fromRational|fst|gcd|getChar|getContents|getLine|group|head|id|inRange|index|init|intToDigit|interact|ioError|isAlpha|isAlphaNum|isAscii|isControl|isDenormalized|isDigit|isHexDigit|isIEEE|isInfinite|isLower|isNaN|isNegativeZero|isOctDigit|isPrint|isSpace|isUpper|iterate|last|lcm|length|lex|lexDigits|lexLitChar|lines|log|logBase|lookup|map|mapM|mapM_|max|maxBound|maximum|maybe|min|minBound|minimum|mod|negate|not|notElem|null|numerator|odd|or|ord|otherwise|pack|pi|pred|primExitWith|print|product|properFraction|putChar|putStr|putStrLn|quot|quotRem|range|rangeSize|read|readDec|readFile|readFloat|readHex|readIO|readInt|readList|readLitChar|readLn|readOct|readParen|readSigned|reads|readsPrec|realToFrac|recip|rem|repeat|replicate|return|reverse|round|scaleFloat|scanl|scanl1|scanr|scanr1|seq|sequence|sequence_|show|showChar|showInt|showList|showLitChar|showParen|showSigned|showString|shows|showsPrec|significand|signum|sin|sinh|snd|sort|span|splitAt|sqrt|subtract|succ|sum|tail|take|takeWhile|tan|tanh|threadToIOResult|toEnum|toInt|toInteger|toLower|toRational|toUpper|truncate|uncurry|undefined|unlines|until|unwords|unzip|unzip3|userError|words|writeFile|zip|zip3|zipWith|zipWith3)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?|0o[0-7]+|0x[0-9a-f]+)\\b", + caseSensitive: false)), + GrammarToken("operator", + compileHighlightPattern("`(?:[A-Z][\\w']*\\.)*[_a-z][\\w']*`"), + greedy: true), + GrammarToken("operator", compileHighlightPattern("(\\s)\\.(?=\\s)"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "[-!#\$%*+=?&@|~:<>^\\\\\\/][-!#\$%*+=?&@|~.:<>^\\\\\\/]*|\\.[-!#\$%*+=?&@|~.:<>^\\\\\\/]+")), + GrammarToken("hvariable", + compileHighlightPattern("\\b(?:[A-Z][\\w']*\\.)*[_a-z][\\w']*"), + inside: () => _g2), + GrammarToken("constant", + compileHighlightPattern("\\b(?:[A-Z][\\w']*\\.)*[A-Z][\\w']*"), + inside: () => _g3), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("keyword", + compileHighlightPattern("\\b(?:as|hiding|import|qualified)\\b")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/html.dart b/lib/highlight/html.dart new file mode 100644 index 0000000..5e42792 --- /dev/null +++ b/lib/highlight/html.dart @@ -0,0 +1,197 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'css.dart'; +import 'js.dart'; + +/// Syntax grammar for `html`. +/// +/// Import this library only when you need `html` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightHtml { + /// The grammar for `html`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", compileHighlightPattern(""), + greedy: true), + GrammarToken("prolog", compileHighlightPattern("<\\?[\\s\\S]+?\\?>"), + greedy: true), + GrammarToken( + "doctype", + compileHighlightPattern( + "\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:[^<\"'\\]]|\"[^\"]*\"|'[^']*'|<(?!!--)|)*\\]\\s*)?>", + caseSensitive: false), + greedy: true, + inside: () => _g1), + GrammarToken( + "style", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "script", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g4), + GrammarToken( + "cdata", + compileHighlightPattern("", + caseSensitive: false), + greedy: true), + GrammarToken( + "tag", + compileHighlightPattern( + "<\\/?(?!\\d)[^\\s>\\/=\$<%]+(?:\\s(?:\\s*[^\\s>\\/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?\\s*\\/?>"), + greedy: true, + inside: () => _g6), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("internal-subset", + compileHighlightPattern("(^[^\\[]*\\[)[\\s\\S]+(?=\\]>\$)"), + lookbehind: true, greedy: true, inside: () => _g0), + GrammarToken("string", compileHighlightPattern("\"[^\"]*\"|'[^']*'"), + greedy: true), + GrammarToken("punctuation", compileHighlightPattern("^\$|[[\\]]")), + GrammarToken( + "doctype-tag", compileHighlightPattern("^DOCTYPE", caseSensitive: false)), + GrammarToken("name", compileHighlightPattern("[^\\s<>'\"]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g3), + GrammarToken("language-css", compileHighlightPattern("[\\s\\S]+"), + inside: () => HighlightCss.grammar), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "language-css", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightCss.grammar), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g5), + GrammarToken("language-javascript", compileHighlightPattern("[\\s\\S]+"), + inside: () => HighlightJs.grammar), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "language-javascript", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightJs.grammar), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g6 = Grammar([ + GrammarToken("tag", compileHighlightPattern("^<\\/?[^\\s>\\/]+"), + inside: () => _g7), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:style)\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel))\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g10), + GrammarToken("attr-value", + compileHighlightPattern("=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+)"), + inside: () => _g12), + GrammarToken("punctuation", compileHighlightPattern("\\/?>")), + GrammarToken("attr-name", compileHighlightPattern("[^\\s>\\/]+"), + inside: () => _g13), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<\\/?")), + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g9), +]); + +final Grammar _g9 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "css", inside: () => HighlightCss.grammar), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g11), +]); + +final Grammar _g11 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "javascript", inside: () => HighlightJs.grammar), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g12 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("^(\\s*)[\"']|[\"']\$"), + lookbehind: true), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g13 = Grammar([ + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); diff --git a/lib/highlight/http.dart b/lib/highlight/http.dart new file mode 100644 index 0000000..6ce9ab1 --- /dev/null +++ b/lib/highlight/http.dart @@ -0,0 +1,137 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'css.dart'; +import 'html.dart'; +import 'js.dart'; +import 'json.dart'; +import 'plain.dart'; +import 'xml.dart'; + +/// Syntax grammar for `http`. +/// +/// Import this library only when you need `http` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightHttp { + /// The grammar for `http`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "request-line", + compileHighlightPattern( + "^(?:CONNECT|DELETE|GET|HEAD|OPTIONS|PATCH|POST|PRI|PUT|SEARCH|TRACE)\\s(?:https?:\\/\\/|\\/)\\S*\\sHTTP\\/[\\d.]+", + multiLine: true), + inside: () => _g1), + GrammarToken("response-status", + compileHighlightPattern("^HTTP\\/[\\d.]+ \\d+ .+", multiLine: true), + inside: () => _g2), + GrammarToken( + "application-javascript", + compileHighlightPattern( + "(content-type:\\s*application\\/javascript(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightJs.grammar), + GrammarToken( + "application-json", + compileHighlightPattern( + "(content-type:\\s*(?:application\\/json|\\w+\\/(?:[\\w.-]+\\+)+json(?![+\\w.-]))(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightJson.grammar), + GrammarToken( + "application-xml", + compileHighlightPattern( + "(content-type:\\s*(?:application\\/xml|\\w+\\/(?:[\\w.-]+\\+)+xml(?![+\\w.-]))(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightXml.grammar), + GrammarToken( + "text-xml", + compileHighlightPattern( + "(content-type:\\s*text\\/xml(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightXml.grammar), + GrammarToken( + "text-html", + compileHighlightPattern( + "(content-type:\\s*text\\/html(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightHtml.grammar), + GrammarToken( + "text-css", + compileHighlightPattern( + "(content-type:\\s*text\\/css(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightCss.grammar), + GrammarToken( + "text-plain", + compileHighlightPattern( + "(content-type:\\s*text\\/plain(?:(?:\\r\\n?|\\n)[\\w-].*)*(?:\\r(?:\\n|(?!\\n))|\\n))[^ \\t\\w-][\\s\\S]*", + caseSensitive: false), + lookbehind: true, + inside: () => HighlightPlain.grammar), + GrammarToken( + "header", + compileHighlightPattern("^[\\w-]+:.+(?:(?:\\r\\n?|\\n)[ \\t].+)*", + multiLine: true), + inside: () => _g3), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("method", compileHighlightPattern("^[A-Z]+\\b"), + alias: "property"), + GrammarToken("request-target", + compileHighlightPattern("^(\\s)(?:https?:\\/\\/|\\/)\\S*(?=\\s)"), + lookbehind: true, alias: "url"), + GrammarToken("http-version", compileHighlightPattern("^(\\s)HTTP\\/[\\d.]+"), + lookbehind: true, alias: "property"), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("http-version", compileHighlightPattern("^HTTP\\/[\\d.]+"), + alias: "property"), + GrammarToken("status-code", compileHighlightPattern("^(\\s)\\d+(?=\\s)"), + lookbehind: true, alias: "number"), + GrammarToken("reason-phrase", compileHighlightPattern("^(\\s).+"), + lookbehind: true, alias: "string"), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "header-value", + compileHighlightPattern( + "(^(?:Content-Security-Policy):[ \t]*(?![ \t]))[^]+", + caseSensitive: false), + lookbehind: true, + alias: "csp"), + GrammarToken( + "header-value", + compileHighlightPattern( + "(^(?:Public-Key-Pins(?:-Report-Only)?):[ \t]*(?![ \t]))[^]+", + caseSensitive: false), + lookbehind: true, + alias: "hpkp"), + GrammarToken( + "header-value", + compileHighlightPattern( + "(^(?:Strict-Transport-Security):[ \t]*(?![ \t]))[^]+", + caseSensitive: false), + lookbehind: true, + alias: "hsts"), + GrammarToken( + "header-value", + compileHighlightPattern("(^(?:[^:]+):[ \t]*(?![ \t]))[^]+", + caseSensitive: false), + lookbehind: true), + GrammarToken("header-name", compileHighlightPattern("^[^:]+"), + alias: "keyword"), + GrammarToken("punctuation", compileHighlightPattern("^:")), +]); diff --git a/lib/highlight/ini.dart b/lib/highlight/ini.dart new file mode 100644 index 0000000..0e23119 --- /dev/null +++ b/lib/highlight/ini.dart @@ -0,0 +1,58 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `ini`. +/// +/// Import this library only when you need `ini` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightIni { + /// The grammar for `ini`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern("(^[ \\f\\t\\v]*)[#;][^\\n\\r]*", + multiLine: true), + lookbehind: true), + GrammarToken( + "section", + compileHighlightPattern("(^[ \\f\\t\\v]*)\\[[^\\n\\r\\]]*\\]?", + multiLine: true), + lookbehind: true, + inside: () => _g1), + GrammarToken( + "key", + compileHighlightPattern( + "(^[ \\f\\t\\v]*)[^ \\f\\n\\r\\t\\v=]+(?:[ \\f\\t\\v]+[^ \\f\\n\\r\\t\\v=]+)*(?=[ \\f\\t\\v]*=)", + multiLine: true), + lookbehind: true, + alias: "attr-name"), + GrammarToken( + "value", + compileHighlightPattern( + "(=[ \\f\\t\\v]*)[^ \\f\\n\\r\\t\\v]+(?:[ \\f\\t\\v]+[^ \\f\\n\\r\\t\\v]+)*"), + lookbehind: true, + alias: "attr-value", + inside: () => _g2), + GrammarToken("punctuation", compileHighlightPattern("=")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "section-name", + compileHighlightPattern( + "(^\\[[ \\f\\t\\v]*)[^ \\f\\t\\v\\]]+(?:[ \\f\\t\\v]+[^ \\f\\t\\v\\]]+)*"), + lookbehind: true, + alias: "selector"), + GrammarToken("punctuation", compileHighlightPattern("\\[|\\]")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("inner-value", compileHighlightPattern("^(\"|').+(?=\\1\$)"), + lookbehind: true), +]); diff --git a/lib/highlight/java.dart b/lib/highlight/java.dart new file mode 100644 index 0000000..3cb0190 --- /dev/null +++ b/lib/highlight/java.dart @@ -0,0 +1,155 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `java`. +/// +/// Import this library only when you need `java` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightJava { + /// The grammar for `java`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "triple-quoted-string", + compileHighlightPattern( + "\"\"\"[ \\t]*[\\r\\n](?:(?:\"|\"\")?(?:\\\\.|[^\"\\\\]))*\"\"\""), + greedy: true, + alias: "string"), + GrammarToken( + "char", compileHighlightPattern("'(?:\\\\.|[^'\\\\\\r\\n]){1,6}'"), + greedy: true), + GrammarToken("string", + compileHighlightPattern("(^|[^\\\\])\"(?:\\\\.|[^\"\\\\\\r\\n])*\""), + lookbehind: true, greedy: true), + GrammarToken("annotation", + compileHighlightPattern("(^|[^.])@\\w+(?:\\s*\\.\\s*\\w+)*"), + lookbehind: true, alias: "punctuation"), + GrammarToken( + "generics", + compileHighlightPattern( + "<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&))*>)*>)*>)*>"), + inside: () => _g1), + GrammarToken( + "import", + compileHighlightPattern( + "(\\bimport\\s+)(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*|\\*)(?=\\s*;)"), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "import", + compileHighlightPattern( + "(\\bimport\\s+static\\s+)(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*(?:\\w+|\\*)(?=\\s*;)"), + lookbehind: true, + alias: "static", + inside: () => _g5), + GrammarToken( + "namespace", + compileHighlightPattern( + "(\\b(?:exports|import(?:\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\s+)(?!\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\\s*[(){}[\\]<>=%~.:,;?+\\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b)[a-z]\\w*(?:\\.[a-z]\\w*)*\\.?"), + lookbehind: true, + inside: () => _g6), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"), + lookbehind: true, + inside: () => _g2), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z]\\w*(?=\\s+\\w+\\s*[;,=()]|\\s*(?:\\[[\\s,]*\\]\\s*)?::\\s*new\\b)"), + lookbehind: true, + inside: () => _g2), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\\s+)(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z]\\w*\\b"), + lookbehind: true, + inside: () => _g2), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\\s*[(){}[\\]<>=%~.:,;?+\\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken("function", compileHighlightPattern("(::\\s*)[a-z_]\\w*"), + lookbehind: true), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0b[01][01_]*L?\\b|\\b0x(?:\\.[\\da-f_p+-]+|[\\da-f_]+(?:\\.[\\da-f_p+-]+)?)\\b|(?:\\b\\d[\\d_]*(?:\\.[\\d_]*)?|\\B\\.\\d[\\d_]*)(?:e[+-]?\\d[\\d_]*)?[dfl]?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "(^|[^.])(?:<<=?|>>>?=?|->|--|\\+\\+|&&|\\|\\||::|[?:~]|[-+*/%&|^!=<>]=?)", + multiLine: true), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken("constant", compileHighlightPattern("\\b[A-Z][A-Z_\\d]+\\b")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"), + lookbehind: true, + inside: () => _g2), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\\s*[(){}[\\]<>=%~.:,;?+\\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[<>(),.:]")), + GrammarToken("operator", compileHighlightPattern("[?&|]")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g3), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g3), + GrammarToken("punctuation", compileHighlightPattern("\\.")), + GrammarToken("operator", compileHighlightPattern("\\*")), + GrammarToken("class-name", compileHighlightPattern("\\w+")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g3), + GrammarToken("static", compileHighlightPattern("\\b\\w+\$")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), + GrammarToken("operator", compileHighlightPattern("\\*")), + GrammarToken("class-name", compileHighlightPattern("\\w+")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/js.dart b/lib/highlight/js.dart new file mode 100644 index 0000000..653a06e --- /dev/null +++ b/lib/highlight/js.dart @@ -0,0 +1,155 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'regex.dart'; + +/// Syntax grammar for `js`. +/// +/// Import this library only when you need `js` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightJs { + /// The grammar for `js`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g1), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g3), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g4), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g0), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, + alias: "language-regex", + inside: () => HighlightRegex.grammar), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); diff --git a/lib/highlight/json.dart b/lib/highlight/json.dart new file mode 100644 index 0000000..ffafcb9 --- /dev/null +++ b/lib/highlight/json.dart @@ -0,0 +1,40 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `json`. +/// +/// Import this library only when you need `json` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightJson { + /// The grammar for `json`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "property", + compileHighlightPattern( + "(^|[^\\\\])\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?=\\s*:)"), + lookbehind: true, + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^\\\\])\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?!\\s*:)"), + lookbehind: true, + greedy: true), + GrammarToken("comment", + compileHighlightPattern("\\/\\/.*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "number", + compileHighlightPattern("-?\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b", + caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\],]")), + GrammarToken("operator", compileHighlightPattern(":")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("null", compileHighlightPattern("\\bnull\\b"), alias: "keyword"), +]); diff --git a/lib/highlight/json5.dart b/lib/highlight/json5.dart new file mode 100644 index 0000000..c3a59fd --- /dev/null +++ b/lib/highlight/json5.dart @@ -0,0 +1,43 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `json5`. +/// +/// Import this library only when you need `json5` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightJson5 { + /// The grammar for `json5`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "property", + compileHighlightPattern( + "(\"|')(?:\\\\(?:\\r\\n?|\\n|.)|(?!\\1)[^\\\\\\r\\n])*\\1(?=\\s*:)"), + greedy: true), + GrammarToken( + "property", + compileHighlightPattern( + "(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)"), + alias: "unquoted"), + GrammarToken( + "string", + compileHighlightPattern( + "(\"|')(?:\\\\(?:\\r\\n?|\\n|.)|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken("comment", + compileHighlightPattern("\\/\\/.*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "number", + compileHighlightPattern( + "[+-]?\\b(?:NaN|Infinity|0x[a-fA-F\\d]+)\\b|[+-]?(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[eE][+-]?\\d+\\b)?")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\],]")), + GrammarToken("operator", compileHighlightPattern(":")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("null", compileHighlightPattern("\\bnull\\b"), alias: "keyword"), +]); diff --git a/lib/highlight/jsx.dart b/lib/highlight/jsx.dart new file mode 100644 index 0000000..ee51fde --- /dev/null +++ b/lib/highlight/jsx.dart @@ -0,0 +1,826 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `jsx`. +/// +/// Import this library only when you need `jsx` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightJsx { + /// The grammar for `jsx`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("prolog", compileHighlightPattern("<\\?[\\s\\S]+?\\?>"), + greedy: true), + GrammarToken( + "doctype", + compileHighlightPattern( + "\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:[^<\"'\\]]|\"[^\"]*\"|'[^']*'|<(?!!--)|)*\\]\\s*)?>", + caseSensitive: false), + greedy: true, + inside: () => _g1), + GrammarToken( + "style", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "script", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g7), + GrammarToken( + "cdata", + compileHighlightPattern("", + caseSensitive: false), + greedy: true), + GrammarToken( + "tag", + compileHighlightPattern( + "<\\/?(?:[\\w.:-]+(?:(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)+(?:[\\w.:\$-]+(?:=(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*'|[^\\s{'\"/>=]+|(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})))?|(?:\\{(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)*\\.{3}(?:[^{}]|(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\}))*\\})))*(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)*\\/?)?>"), + greedy: true, + inside: () => _g19), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g28), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g31), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g32), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("internal-subset", + compileHighlightPattern("(^[^\\[]*\\[)[\\s\\S]+(?=\\]>\$)"), + lookbehind: true, greedy: true, inside: () => _g0), + GrammarToken("string", compileHighlightPattern("\"[^\"]*\"|'[^']*'"), + greedy: true), + GrammarToken("punctuation", compileHighlightPattern("^\$|[[\\]]")), + GrammarToken( + "doctype-tag", compileHighlightPattern("^DOCTYPE", caseSensitive: false)), + GrammarToken("name", compileHighlightPattern("[^\\s<>'\"]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g3), + GrammarToken("language-css", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g4), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "language-css", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/")), + GrammarToken( + "atrule", + compileHighlightPattern( + "@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*?(?:;|(?=\\s*\\{))"), + inside: () => _g5), + GrammarToken( + "url", + compileHighlightPattern( + "\\burl\\((?:(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')|(?:[^\\\\\\r\\n()\"']|\\\\[\\s\\S])*)\\)", + caseSensitive: false), + greedy: true, + inside: () => _g6), + GrammarToken( + "selector", + compileHighlightPattern( + "(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*(?=\\s*\\{)"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken( + "property", + compileHighlightPattern( + "(^|[^-\\w\\xA0-\\uFFFF])(?!\\s)[-_a-z\\xA0-\\uFFFF](?:(?!\\s)[-\\w\\xA0-\\uFFFF])*(?=\\s*:)", + caseSensitive: false), + lookbehind: true), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("rule", compileHighlightPattern("^@[\\w-]+")), + GrammarToken( + "selector-function-argument", + compileHighlightPattern( + "(\\bselector\\s*\\(\\s*(?![\\s)]))(?:[^()\\s]|\\s+(?![\\s)])|\\((?:[^()]|\\([^()]*\\))*\\))+(?=\\s*\\))"), + lookbehind: true, + alias: "selector"), + GrammarToken("keyword", + compileHighlightPattern("(^|[^\\w-])(?:and|not|only|or)(?![\\w-])"), + lookbehind: true), +], rest: () => _g4); + +final Grammar _g6 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^url", caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("^\\(|\\)\$")), + GrammarToken( + "string", + compileHighlightPattern( + "^(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')\$"), + alias: "url"), +]); + +final Grammar _g7 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g8), + GrammarToken("language-javascript", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g9), +]); + +final Grammar _g8 = Grammar([ + GrammarToken( + "language-javascript", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g9 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g10), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g12), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g13), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g11), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g11 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g9); + +final Grammar _g12 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g13 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g14), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g14 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g15), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g17), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g18), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g15 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g16), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g16 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g17 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g18 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g19 = Grammar([ + GrammarToken("tag", compileHighlightPattern("^<\\/?[^\\s>\\/]*"), + inside: () => _g20), + GrammarToken( + "script", + compileHighlightPattern( + "=(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"), + alias: "language-javascript", + inside: () => _g21), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:style)\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g22), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel))\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g24), + GrammarToken( + "attr-value", + compileHighlightPattern( + "=(?!\\{)(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*'|[^\\s'\">]+)"), + inside: () => _g26), + GrammarToken("punctuation", compileHighlightPattern("\\/?>")), + GrammarToken( + "spread", + compileHighlightPattern( + "(?:\\{(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)*\\.{3}(?:[^{}]|(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\}))*\\})"), + inside: () => _g0), + GrammarToken("attr-name", compileHighlightPattern("[^\\s>\\/]+"), + inside: () => _g27), + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), +]); + +final Grammar _g20 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<\\/?")), + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), + GrammarToken( + "class-name", compileHighlightPattern("^[A-Z]\\w*(?:\\.[A-Z]\\w*)*\$")), +]); + +final Grammar _g21 = Grammar([ + GrammarToken("script-punctuation", compileHighlightPattern("^=(?=\\{)"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g22 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g23), +]); + +final Grammar _g23 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "css", inside: () => _g4), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g24 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g25), +]); + +final Grammar _g25 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "javascript", inside: () => _g9), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g26 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("^(\\s*)[\"']|[\"']\$"), + lookbehind: true), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g27 = Grammar([ + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); + +final Grammar _g28 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g29), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g29 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g30); + +final Grammar _g30 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g28), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g31), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g32), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g30), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g31 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g32 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g33), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g33 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g34), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g36), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g37), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g34 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g35), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g35 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g36 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g37 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); diff --git a/lib/highlight/julia.dart b/lib/highlight/julia.dart new file mode 100644 index 0000000..7d33557 --- /dev/null +++ b/lib/highlight/julia.dart @@ -0,0 +1,53 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `julia`. +/// +/// Import this library only when you need `julia` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightJulia { + /// The grammar for `julia`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "(^|[^\\\\])(?:#=(?:[^#=]|=(?!#)|#(?!=)|#=(?:[^#=]|=(?!#)|#(?!=))*=#)*=#|#.*)"), + lookbehind: true), + GrammarToken("regex", + compileHighlightPattern("r\"(?:\\\\.|[^\"\\\\\\r\\n])*\"[imsx]{0,4}"), + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"\"\"[\\s\\S]+?\"\"\"|(?:\\b\\w+)?\"(?:\\\\.|[^\"\\\\\\r\\n])*\"|`(?:[^\\\\`\\r\\n]|\\\\.)*`"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "(^|[^\\w'])'(?:\\\\[^\\r\\n][^'\\r\\n]*|[^\\\\\\r\\n])'"), + lookbehind: true, + greedy: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|baremodule|begin|bitstype|break|catch|ccall|const|continue|do|else|elseif|end|export|finally|for|function|global|if|immutable|import|importall|in|let|local|macro|module|print|println|quote|return|struct|try|type|typealias|using|while)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b(?=\\d)|\\B(?=\\.))(?:0[box])?(?:[\\da-f]+(?:_[\\da-f]+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[efp][+-]?\\d+(?:_\\d+)*)?j?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "&&|\\|\\||[-+*^%รทโŠป&\$\\\\]=?|\\/[\\/=]?|!=?=?|\\|[=>]?|<(?:<=?|[=:|])?|>(?:=|>>?=?)?|==?=?|[~โ‰ โ‰คโ‰ฅ'โˆšโˆ›]")), + GrammarToken("punctuation", compileHighlightPattern("::?|[{}[\\]();,.?]")), + GrammarToken("constant", + compileHighlightPattern("\\b(?:(?:Inf|NaN)(?:16|32|64)?|im|pi)\\b|[ฯ€โ„ฏ]")), +]); diff --git a/lib/highlight/kotlin.dart b/lib/highlight/kotlin.dart new file mode 100644 index 0000000..2d6ec8b --- /dev/null +++ b/lib/highlight/kotlin.dart @@ -0,0 +1,93 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `kotlin`. +/// +/// Import this library only when you need `kotlin` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightKotlin { + /// The grammar for `kotlin`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string-literal", + compileHighlightPattern( + "\"\"\"(?:[^\$]|\\\$(?:(?!\\{)|\\{[^{}]*\\}))*?\"\"\""), + alias: "multiline", + inside: () => _g1), + GrammarToken( + "string-literal", + compileHighlightPattern( + "\"(?:[^\"\\\\\\r\\n\$]|\\\\.|\\\$(?:(?!\\{)|\\{[^{}]*\\}))*\""), + alias: "singleline", + inside: () => _g3), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:[^'\\\\\\r\\n]|\\\\(?:.|u[a-fA-F0-9]{0,4}))'"), + greedy: true), + GrammarToken("annotation", + compileHighlightPattern("\\B@(?:\\w+:)?(?:[A-Z]\\w*|\\[[^\\]]+\\])"), + alias: "builtin"), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.])\\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("label", compileHighlightPattern("\\b\\w+@|@\\w+\\b"), + alias: "symbol"), + GrammarToken("function", + compileHighlightPattern("(?:`[^\\r\\n`]+`|\\b\\w+)(?=\\s*\\()"), + greedy: true), + GrammarToken("function", + compileHighlightPattern("(\\.)(?:`[^\\r\\n`]+`|\\w+)(?=\\s*\\{)"), + lookbehind: true, greedy: true), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\\d+(?:_\\d+)*(?:\\.\\d+(?:_\\d+)*)?(?:[eE][+-]?\\d+(?:_\\d+)*)?[fFL]?)\\b")), + GrammarToken( + "operator", + compileHighlightPattern( + "\\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\\/*%<>]=?|[?:]:?|\\.\\.|&&|\\|\\||\\b(?:and|inv|or|shl|shr|ushr|xor)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern("\\\$(?:[a-z_]\\w*|\\{[^{}]*\\})", + caseSensitive: false), + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{?|\\}\$"), + alias: "punctuation"), + GrammarToken("expression", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g0), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$(?:[a-z_]\\w*|\\{[^{}]*\\})", + caseSensitive: false), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); diff --git a/lib/highlight/latex.dart b/lib/highlight/latex.dart new file mode 100644 index 0000000..ae3cddf --- /dev/null +++ b/lib/highlight/latex.dart @@ -0,0 +1,63 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `latex`. +/// +/// Import this library only when you need `latex` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightLatex { + /// The grammar for `latex`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("%.*")), + GrammarToken( + "cdata", + compileHighlightPattern( + "(\\\\begin\\{((?:lstlisting|verbatim)\\*?)\\})[\\s\\S]*?(?=\\\\end\\{\\2\\})"), + lookbehind: true), + GrammarToken( + "equation", + compileHighlightPattern( + "\\\$\\\$(?:\\\\[\\s\\S]|[^\\\\\$])+\\\$\\\$|\\\$(?:\\\\[\\s\\S]|[^\\\\\$])+\\\$|\\\\\\([\\s\\S]*?\\\\\\)|\\\\\\[[\\s\\S]*?\\\\\\]"), + alias: "string", + inside: () => _g1), + GrammarToken( + "equation", + compileHighlightPattern( + "(\\\\begin\\{((?:align|eqnarray|equation|gather|math|multline)\\*?)\\})[\\s\\S]*?(?=\\\\end\\{\\2\\})"), + lookbehind: true, + alias: "string", + inside: () => _g1), + GrammarToken( + "keyword", + compileHighlightPattern( + "(\\\\(?:begin|cite|documentclass|end|label|ref|usepackage)(?:\\[[^\\]]+\\])?\\{)[^}]+(?=\\})"), + lookbehind: true), + GrammarToken("url", compileHighlightPattern("(\\\\url\\{)[^}]+(?=\\})"), + lookbehind: true), + GrammarToken( + "headline", + compileHighlightPattern( + "(\\\\(?:chapter|frametitle|paragraph|part|section|subparagraph|subsection|subsubparagraph|subsubsection|subsubsubparagraph)\\*?(?:\\[[^\\]]+\\])?\\{)[^}]+(?=\\})"), + lookbehind: true, + alias: "class-name"), + GrammarToken( + "function", + compileHighlightPattern("\\\\(?:[^a-z()[\\]]|[a-z*]+)", + caseSensitive: false), + alias: "selector"), + GrammarToken("punctuation", compileHighlightPattern("[[\\]{}&]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "equation-command", + compileHighlightPattern("\\\\(?:[^a-z()[\\]]|[a-z*]+)", + caseSensitive: false), + alias: "regex"), +]); diff --git a/lib/highlight/less.dart b/lib/highlight/less.dart new file mode 100644 index 0000000..0ed786c --- /dev/null +++ b/lib/highlight/less.dart @@ -0,0 +1,82 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `less`. +/// +/// Import this library only when you need `less` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightLess { + /// The grammar for `less`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/")), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\])\\/\\/.*"), + lookbehind: true), + GrammarToken( + "atrule", + compileHighlightPattern( + "@[\\w-](?:\\((?:[^(){}]|\\([^(){}]*\\))*\\)|[^(){};\\s]|\\s+(?!\\s))*?(?=\\s*\\{)"), + inside: () => _g1), + GrammarToken( + "url", + compileHighlightPattern( + "\\burl\\((?:(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')|(?:[^\\\\\\r\\n()\"']|\\\\[\\s\\S])*)\\)", + caseSensitive: false), + greedy: true, + inside: () => _g2), + GrammarToken( + "selector", + compileHighlightPattern( + "(?:@\\{[\\w-]+\\}|[^{};\\s@])(?:@\\{[\\w-]+\\}|\\((?:[^(){}]|\\([^(){}]*\\))*\\)|[^(){};@\\s]|\\s+(?!\\s))*?(?=\\s*\\{)"), + inside: () => _g3), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken("variable", compileHighlightPattern("@[\\w-]+\\s*:"), + inside: () => _g4), + GrammarToken("variable", compileHighlightPattern("@@?[\\w-]+")), + GrammarToken("mixin-usage", + compileHighlightPattern("([{;]\\s*)[.#](?!\\d)[\\w-].*?(?=[(;])"), + lookbehind: true, alias: "function"), + GrammarToken("property", + compileHighlightPattern("(?:@\\{[\\w-]+\\}|[\\w-])+(?:\\+_?)?(?=\\s*:)")), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), + GrammarToken("operator", compileHighlightPattern("[+\\-*\\/]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[:()]")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^url", caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("^\\(|\\)\$")), + GrammarToken( + "string", + compileHighlightPattern( + "^(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')\$"), + alias: "url"), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("variable", compileHighlightPattern("@+[\\w-]+")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern(":")), +]); diff --git a/lib/highlight/lua.dart b/lib/highlight/lua.dart new file mode 100644 index 0000000..c296cb0 --- /dev/null +++ b/lib/highlight/lua.dart @@ -0,0 +1,43 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `lua`. +/// +/// Import this library only when you need `lua` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightLua { + /// The grammar for `lua`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern("^#!.+|--(?:\\[(=*)\\[[\\s\\S]*?\\]\\1\\]|.*)", + multiLine: true)), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:(?!\\1)[^\\\\\\r\\n]|\\\\z(?:\\r\\n|\\s)|\\\\(?:\\r\\n|[^z]))*\\1|\\[(=*)\\[[\\s\\S]*?\\]\\2\\]"), + greedy: true), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[a-f\\d]+(?:\\.[a-f\\d]*)?(?:p[+-]?\\d+)?\\b|\\b\\d+(?:\\.\\B|(?:\\.\\d*)?(?:e[+-]?\\d+)?\\b)|\\B\\.\\d+(?:e[+-]?\\d+)?\\b", + caseSensitive: false)), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\\b")), + GrammarToken( + "function", compileHighlightPattern("(?!\\d)\\w+(?=\\s*(?:[({]))")), + GrammarToken("operator", + compileHighlightPattern("[-+*%^&|#]|\\/\\/?|<[<=]?|>[>=]?|[=~]=?")), + GrammarToken("operator", compileHighlightPattern("(^|[^.])\\.\\.(?!\\.)"), + lookbehind: true), + GrammarToken( + "punctuation", compileHighlightPattern("[\\[\\](){},;]|\\.+|:+")), +]); diff --git a/lib/highlight/makefile.dart b/lib/highlight/makefile.dart new file mode 100644 index 0000000..576a0d5 --- /dev/null +++ b/lib/highlight/makefile.dart @@ -0,0 +1,56 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `makefile`. +/// +/// Import this library only when you need `makefile` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightMakefile { + /// The grammar for `makefile`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "(^|[^\\\\])#(?:\\\\(?:\\r\\n|[\\s\\S])|[^\\\\\\r\\n])*"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken("builtin-target", + compileHighlightPattern("\\.[A-Z][^:#=\\s]+(?=\\s*:(?!=))"), + alias: "builtin"), + GrammarToken( + "target", + compileHighlightPattern("^(?:[^:=\\s]|[ \\t]+(?![\\s:]))+(?=\\s*:(?!=))", + multiLine: true), + alias: "symbol", + inside: () => _g1), + GrammarToken( + "variable", + compileHighlightPattern( + "\\\$+(?:(?!\\\$)[^(){}:#=\\s]+|\\([@*%<^+?][DF]\\)|(?=[({]))")), + GrammarToken( + "keyword", + compileHighlightPattern( + "-include\\b|\\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "(\\()(?:abspath|addsuffix|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:list|s)?)(?=[ \\t])"), + lookbehind: true), + GrammarToken("operator", compileHighlightPattern("(?:::|[?:+!])?=|[|@]")), + GrammarToken("punctuation", compileHighlightPattern("[:;(){}]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("variable", + compileHighlightPattern("\\\$+(?:(?!\\\$)[^(){}:#=\\s]+|(?=[({]))")), +]); diff --git a/lib/highlight/markdown.dart b/lib/highlight/markdown.dart new file mode 100644 index 0000000..f67e546 --- /dev/null +++ b/lib/highlight/markdown.dart @@ -0,0 +1,801 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +import 'yaml.dart'; + +/// Syntax grammar for `markdown`. +/// +/// Import this library only when you need `markdown` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightMarkdown { + /// The grammar for `markdown`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", compileHighlightPattern(""), + greedy: true), + GrammarToken( + "front-matter-block", + compileHighlightPattern( + "(^(?:\\s*[\\r\\n])?)---(?!.)[\\s\\S]*?[\\r\\n]---(?!.)"), + lookbehind: true, + greedy: true, + inside: () => _g1), + GrammarToken( + "blockquote", compileHighlightPattern("^>(?:[\\t ]*>)*", multiLine: true), + alias: "punctuation"), + GrammarToken( + "table", + compileHighlightPattern( + "^\\|?(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+(?:\\|(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+)+\\|?(?:(?:\\n|\\r\\n?)|(?![\\s\\S]))\\|?[ \\t]*:?-{3,}:?[ \\t]*(?:\\|[ \\t]*:?-{3,}:?[ \\t]*)+\\|?(?:\\n|\\r\\n?)(?:\\|?(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+(?:\\|(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+)+\\|?(?:(?:\\n|\\r\\n?)|(?![\\s\\S])))*", + multiLine: true), + inside: () => _g2), + GrammarToken( + "code", + compileHighlightPattern( + "((?:^|\\n)[ \\t]*\\n|(?:^|\\r\\n?)[ \\t]*\\r\\n?)(?: {4}|\\t).+(?:(?:\\n|\\r\\n?)(?: {4}|\\t).+)*"), + lookbehind: true, + alias: "keyword"), + GrammarToken( + "code", compileHighlightPattern("^```[\\s\\S]*?^```\$", multiLine: true), + greedy: true, inside: () => _g6), + GrammarToken( + "title", + compileHighlightPattern("\\S.*(?:\\n|\\r\\n?)(?:==+|--+)(?=[ \\t]*\$)", + multiLine: true), + alias: "important", + inside: () => _g7), + GrammarToken("title", compileHighlightPattern("(^\\s*)#.+", multiLine: true), + lookbehind: true, alias: "important", inside: () => _g8), + GrammarToken( + "hr", + compileHighlightPattern("(^\\s*)([*-])(?:[\\t ]*\\2){2,}(?=\\s*\$)", + multiLine: true), + lookbehind: true, + alias: "punctuation"), + GrammarToken( + "list", + compileHighlightPattern("(^\\s*)(?:[*+-]|\\d+\\.)(?=[\\t ].)", + multiLine: true), + lookbehind: true, + alias: "punctuation"), + GrammarToken( + "url-reference", + compileHighlightPattern( + "!?\\[[^\\]]+\\]:[\\t ]+(?:\\S+|<(?:\\\\.|[^>\\\\])+>)(?:[\\t ]+(?:\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|\\((?:\\\\.|[^)\\\\])*\\)))?"), + alias: "url", + inside: () => _g9), + GrammarToken( + "bold", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+_)+__\\b|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*)+\\*\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g10), + GrammarToken( + "italic", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+__)+_\\b|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*\\*)+\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g14), + GrammarToken( + "strike", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:(~~?)(?:(?!~)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\2)"), + lookbehind: true, + greedy: true, + inside: () => _g16), + GrammarToken( + "code-snippet", + compileHighlightPattern( + "(^|[^\\\\`])(?:``[^`\\r\\n]+(?:`[^`\\r\\n]+)*``(?!`)|`[^`\\r\\n]+`(?!`))"), + lookbehind: true, + greedy: true, + alias: "code"), + GrammarToken( + "url", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:!?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\](?:\\([^\\s)]+(?:[\\t ]+\"(?:\\\\.|[^\"\\\\])*\")?\\)|[ \\t]?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\]))"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken("prolog", compileHighlightPattern("<\\?[\\s\\S]+?\\?>"), + greedy: true), + GrammarToken( + "doctype", + compileHighlightPattern( + "\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:[^<\"'\\]]|\"[^\"]*\"|'[^']*'|<(?!!--)|)*\\]\\s*)?>", + caseSensitive: false), + greedy: true, + inside: () => _g18), + GrammarToken( + "style", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g19), + GrammarToken( + "script", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g24), + GrammarToken( + "cdata", + compileHighlightPattern("", + caseSensitive: false), + greedy: true), + GrammarToken( + "tag", + compileHighlightPattern( + "<\\/?(?!\\d)[^\\s>\\/=\$<%]+(?:\\s(?:\\s*[^\\s>\\/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?\\s*\\/?>"), + greedy: true, + inside: () => _g36), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^---|---\$")), + GrammarToken("front-matter", compileHighlightPattern("\\S+(?:\\s+\\S+)*"), + alias: "yaml", inside: () => HighlightYaml.grammar), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "table-data-rows", + compileHighlightPattern( + "^(\\|?(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+(?:\\|(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+)+\\|?(?:(?:\\n|\\r\\n?)|(?![\\s\\S]))\\|?[ \\t]*:?-{3,}:?[ \\t]*(?:\\|[ \\t]*:?-{3,}:?[ \\t]*)+\\|?(?:\\n|\\r\\n?))(?:\\|?(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+(?:\\|(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+)+\\|?(?:(?:\\n|\\r\\n?)|(?![\\s\\S])))*\$"), + lookbehind: true, + inside: () => _g3), + GrammarToken( + "table-line", + compileHighlightPattern( + "^(\\|?(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+(?:\\|(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+)+\\|?(?:(?:\\n|\\r\\n?)|(?![\\s\\S])))\\|?[ \\t]*:?-{3,}:?[ \\t]*(?:\\|[ \\t]*:?-{3,}:?[ \\t]*)+\\|?(?:\\n|\\r\\n?)\$"), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "table-header-row", + compileHighlightPattern( + "^\\|?(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+(?:\\|(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+)+\\|?(?:(?:\\n|\\r\\n?)|(?![\\s\\S]))\$"), + inside: () => _g5), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "table-data", + compileHighlightPattern( + "(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+"), + inside: () => _g0), + GrammarToken("punctuation", compileHighlightPattern("\\|")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\||:?-{3,}:?")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "table-header", + compileHighlightPattern( + "(?:\\\\.|``(?:[^`\\r\\n]|`(?!`))+``|`[^`\\r\\n]+`|[^\\\\|\\r\\n`])+"), + alias: "important", + inside: () => _g0), + GrammarToken("punctuation", compileHighlightPattern("\\|")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "code-block", + compileHighlightPattern( + "^(```.*(?:\\n|\\r\\n?))[\\s\\S]+?(?=(?:\\n|\\r\\n?)^```\$)", + multiLine: true), + lookbehind: true), + GrammarToken("code-language", compileHighlightPattern("^(```).+"), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("```")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("==+\$|--+\$")), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^#+|#+\$")), +]); + +final Grammar _g9 = Grammar([ + GrammarToken("variable", compileHighlightPattern("^(!?\\[)[^\\]]+"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*'|\\((?:\\\\.|[^)\\\\])*\\))\$")), + GrammarToken("punctuation", compileHighlightPattern("^[\\[\\]!:]|[<>]")), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("content", compileHighlightPattern("(^..)[\\s\\S]+(?=..\$)"), + lookbehind: true, inside: () => _g11), + GrammarToken("punctuation", compileHighlightPattern("\\*\\*|__")), +]); + +final Grammar _g11 = Grammar([ + GrammarToken( + "url", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:!?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\](?:\\([^\\s)]+(?:[\\t ]+\"(?:\\\\.|[^\"\\\\])*\")?\\)|[ \\t]?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\]))"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken( + "italic", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+__)+_\\b|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*\\*)+\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g14), + GrammarToken( + "strike", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:(~~?)(?:(?!~)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\2)"), + lookbehind: true, + greedy: true, + inside: () => _g16), + GrammarToken( + "code-snippet", + compileHighlightPattern( + "(^|[^\\\\`])(?:``[^`\\r\\n]+(?:`[^`\\r\\n]+)*``(?!`)|`[^`\\r\\n]+`(?!`))"), + lookbehind: true, + greedy: true, + alias: "code"), +]); + +final Grammar _g12 = Grammar([ + GrammarToken("operator", compileHighlightPattern("^!")), + GrammarToken("content", compileHighlightPattern("(^\\[)[^\\]]+(?=\\])"), + lookbehind: true, inside: () => _g13), + GrammarToken( + "variable", compileHighlightPattern("(^\\][ \\t]?\\[)[^\\]]+(?=\\]\$)"), + lookbehind: true), + GrammarToken("url", compileHighlightPattern("(^\\]\\()[^\\s)]+"), + lookbehind: true), + GrammarToken("string", + compileHighlightPattern("(^[ \\t]+)\"(?:\\\\.|[^\"\\\\])*\"(?=\\)\$)"), + lookbehind: true), +]); + +final Grammar _g13 = Grammar([ + GrammarToken( + "bold", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+_)+__\\b|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*)+\\*\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g10), + GrammarToken( + "italic", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+__)+_\\b|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*\\*)+\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g14), + GrammarToken( + "strike", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:(~~?)(?:(?!~)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\2)"), + lookbehind: true, + greedy: true, + inside: () => _g16), + GrammarToken( + "code-snippet", + compileHighlightPattern( + "(^|[^\\\\`])(?:``[^`\\r\\n]+(?:`[^`\\r\\n]+)*``(?!`)|`[^`\\r\\n]+`(?!`))"), + lookbehind: true, + greedy: true, + alias: "code"), +]); + +final Grammar _g14 = Grammar([ + GrammarToken("content", compileHighlightPattern("(^.)[\\s\\S]+(?=.\$)"), + lookbehind: true, inside: () => _g15), + GrammarToken("punctuation", compileHighlightPattern("[*_]")), +]); + +final Grammar _g15 = Grammar([ + GrammarToken( + "url", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:!?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\](?:\\([^\\s)]+(?:[\\t ]+\"(?:\\\\.|[^\"\\\\])*\")?\\)|[ \\t]?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\]))"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken( + "bold", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+_)+__\\b|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*)+\\*\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g10), + GrammarToken( + "strike", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:(~~?)(?:(?!~)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\2)"), + lookbehind: true, + greedy: true, + inside: () => _g16), + GrammarToken( + "code-snippet", + compileHighlightPattern( + "(^|[^\\\\`])(?:``[^`\\r\\n]+(?:`[^`\\r\\n]+)*``(?!`)|`[^`\\r\\n]+`(?!`))"), + lookbehind: true, + greedy: true, + alias: "code"), +]); + +final Grammar _g16 = Grammar([ + GrammarToken("content", compileHighlightPattern("(^~~?)[\\s\\S]+(?=\\1\$)"), + lookbehind: true, inside: () => _g17), + GrammarToken("punctuation", compileHighlightPattern("~~?")), +]); + +final Grammar _g17 = Grammar([ + GrammarToken( + "url", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:!?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\](?:\\([^\\s)]+(?:[\\t ]+\"(?:\\\\.|[^\"\\\\])*\")?\\)|[ \\t]?\\[(?:(?!\\])(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\]))"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken( + "bold", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+_)+__\\b|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*)+\\*\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g10), + GrammarToken( + "italic", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)(?:\\b_(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|__(?:(?!_)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+__)+_\\b|\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n]))|\\*\\*(?:(?!\\*)(?:\\\\.|[^\\\\\\n\\r]|(?:\\n|\\r\\n?)(?![\\r\\n])))+\\*\\*)+\\*)"), + lookbehind: true, + greedy: true, + inside: () => _g14), + GrammarToken( + "code-snippet", + compileHighlightPattern( + "(^|[^\\\\`])(?:``[^`\\r\\n]+(?:`[^`\\r\\n]+)*``(?!`)|`[^`\\r\\n]+`(?!`))"), + lookbehind: true, + greedy: true, + alias: "code"), +]); + +final Grammar _g18 = Grammar([ + GrammarToken("internal-subset", + compileHighlightPattern("(^[^\\[]*\\[)[\\s\\S]+(?=\\]>\$)"), + lookbehind: true, greedy: true, inside: () => _g0), + GrammarToken("string", compileHighlightPattern("\"[^\"]*\"|'[^']*'"), + greedy: true), + GrammarToken("punctuation", compileHighlightPattern("^\$|[[\\]]")), + GrammarToken( + "doctype-tag", compileHighlightPattern("^DOCTYPE", caseSensitive: false)), + GrammarToken("name", compileHighlightPattern("[^\\s<>'\"]+")), +]); + +final Grammar _g19 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g20), + GrammarToken("language-css", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g21), +]); + +final Grammar _g20 = Grammar([ + GrammarToken( + "language-css", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => _g21), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g21 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/")), + GrammarToken( + "atrule", + compileHighlightPattern( + "@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*?(?:;|(?=\\s*\\{))"), + inside: () => _g22), + GrammarToken( + "url", + compileHighlightPattern( + "\\burl\\((?:(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')|(?:[^\\\\\\r\\n()\"']|\\\\[\\s\\S])*)\\)", + caseSensitive: false), + greedy: true, + inside: () => _g23), + GrammarToken( + "selector", + compileHighlightPattern( + "(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*(?=\\s*\\{)"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken( + "property", + compileHighlightPattern( + "(^|[^-\\w\\xA0-\\uFFFF])(?!\\s)[-_a-z\\xA0-\\uFFFF](?:(?!\\s)[-\\w\\xA0-\\uFFFF])*(?=\\s*:)", + caseSensitive: false), + lookbehind: true), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), +]); + +final Grammar _g22 = Grammar([ + GrammarToken("rule", compileHighlightPattern("^@[\\w-]+")), + GrammarToken( + "selector-function-argument", + compileHighlightPattern( + "(\\bselector\\s*\\(\\s*(?![\\s)]))(?:[^()\\s]|\\s+(?![\\s)])|\\((?:[^()]|\\([^()]*\\))*\\))+(?=\\s*\\))"), + lookbehind: true, + alias: "selector"), + GrammarToken("keyword", + compileHighlightPattern("(^|[^\\w-])(?:and|not|only|or)(?![\\w-])"), + lookbehind: true), +], rest: () => _g21); + +final Grammar _g23 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^url", caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("^\\(|\\)\$")), + GrammarToken( + "string", + compileHighlightPattern( + "^(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')\$"), + alias: "url"), +]); + +final Grammar _g24 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g25), + GrammarToken("language-javascript", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g26), +]); + +final Grammar _g25 = Grammar([ + GrammarToken( + "language-javascript", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => _g26), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g26 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g27), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g29), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g30), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g26), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g26), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g26), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g26), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g27 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g28), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g28 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g26); + +final Grammar _g29 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g30 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g31), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g31 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g32), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g34), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g35), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g32 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g33), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g33 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g34 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g35 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g36 = Grammar([ + GrammarToken("tag", compileHighlightPattern("^<\\/?[^\\s>\\/]+"), + inside: () => _g37), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:style)\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g38), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel))\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g40), + GrammarToken("attr-value", + compileHighlightPattern("=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+)"), + inside: () => _g42), + GrammarToken("punctuation", compileHighlightPattern("\\/?>")), + GrammarToken("attr-name", compileHighlightPattern("[^\\s>\\/]+"), + inside: () => _g43), +]); + +final Grammar _g37 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<\\/?")), + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); + +final Grammar _g38 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g39), +]); + +final Grammar _g39 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "css", inside: () => _g21), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g40 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g41), +]); + +final Grammar _g41 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "javascript", inside: () => _g26), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g42 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("^(\\s*)[\"']|[\"']\$"), + lookbehind: true), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g43 = Grammar([ + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); diff --git a/lib/highlight/markup_templating.dart b/lib/highlight/markup_templating.dart new file mode 100644 index 0000000..4869c5c --- /dev/null +++ b/lib/highlight/markup_templating.dart @@ -0,0 +1,16 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `markup-templating`. +/// +/// Import this library only when you need `markup-templating` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightMarkupTemplating { + /// The grammar for `markup-templating`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([]); diff --git a/lib/highlight/nginx.dart b/lib/highlight/nginx.dart new file mode 100644 index 0000000..ff4cc0d --- /dev/null +++ b/lib/highlight/nginx.dart @@ -0,0 +1,60 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `nginx`. +/// +/// Import this library only when you need `nginx` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightNginx { + /// The grammar for `nginx`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("(^|[\\s{};])#.*"), + lookbehind: true, greedy: true), + GrammarToken( + "directive", + compileHighlightPattern( + "(^|\\s)\\w(?:[^;{}\"'\\\\\\s]|\\\\.|\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|\\s+(?:#.*(?!.)|(?![#\\s])))*?(?=\\s*[;{])"), + lookbehind: true, + greedy: true, + inside: () => _g1), + GrammarToken("punctuation", compileHighlightPattern("[{};]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "string", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)(?:\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*')"), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken("comment", compileHighlightPattern("(\\s)#.*"), + lookbehind: true, greedy: true), + GrammarToken("keyword", compileHighlightPattern("^\\S+"), greedy: true), + GrammarToken("boolean", compileHighlightPattern("(\\s)(?:off|on)(?!\\S)"), + lookbehind: true), + GrammarToken("number", + compileHighlightPattern("(\\s)\\d+[a-z]*(?!\\S)", caseSensitive: false), + lookbehind: true), + GrammarToken( + "variable", + compileHighlightPattern( + "\\\$(?:\\w[a-z\\d]*(?:_[^\\x00-\\x1F\\s\"'\\\\()\$]*)?|\\{[^}\\s\"'\\\\]+\\})", + caseSensitive: false)), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("escape", compileHighlightPattern("\\\\[\"'\\\\nrt]"), + alias: "entity"), + GrammarToken( + "variable", + compileHighlightPattern( + "\\\$(?:\\w[a-z\\d]*(?:_[^\\x00-\\x1F\\s\"'\\\\()\$]*)?|\\{[^}\\s\"'\\\\]+\\})", + caseSensitive: false)), +]); diff --git a/lib/highlight/objectivec.dart b/lib/highlight/objectivec.dart new file mode 100644 index 0000000..98def62 --- /dev/null +++ b/lib/highlight/objectivec.dart @@ -0,0 +1,101 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `objectivec`. +/// +/// Import this library only when you need `objectivec` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightObjectivec { + /// The grammar for `objectivec`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "macro", + compileHighlightPattern( + "(^[\\t ]*)#\\s*[a-z](?:[^\\r\\n\\\\/]|\\/(?!\\*)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\\\\(?:\\r\\n|[\\s\\S]))*", + caseSensitive: false, + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property", + inside: () => _g1), + GrammarToken( + "string", + compileHighlightPattern( + "@?\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\\b")), + GrammarToken( + "constant", + compileHighlightPattern( + "\\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0x(?:[\\da-f]+(?:\\.[\\da-f]*)?|\\.[\\da-f]+)(?:p[+-]?\\d+)?|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?)[ful]{0,4}", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "-[->]?|\\+\\+?|!=?|<>?=?|==?|&&?|\\|\\|?|[~^%?*\\/@]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("string", compileHighlightPattern("^(#\\s*include\\s*)<[^>]+>"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\""), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n]){0,32}'"), + greedy: true), + GrammarToken( + "comment", + compileHighlightPattern( + "\\/\\/(?:[^\\r\\n\\\\]|\\\\(?:\\r\\n?|\\n|(?![\\r\\n])))*|\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + greedy: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?!\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "macro-name", + compileHighlightPattern("(^#\\s*define\\s+)\\w+\\b(?=\\()", + caseSensitive: false), + lookbehind: true, + alias: "function"), + GrammarToken("directive", compileHighlightPattern("^(#\\s*)[a-z]+"), + lookbehind: true, alias: "keyword"), + GrammarToken("directive-hash", compileHighlightPattern("^#")), + GrammarToken("punctuation", compileHighlightPattern("##|\\\\(?=[\\r\\n])")), + GrammarToken("expression", compileHighlightPattern("\\S[\\s\\S]*"), + inside: () => _g0), +]); diff --git a/lib/highlight/ocaml.dart b/lib/highlight/ocaml.dart new file mode 100644 index 0000000..51b7377 --- /dev/null +++ b/lib/highlight/ocaml.dart @@ -0,0 +1,67 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `ocaml`. +/// +/// Import this library only when you need `ocaml` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightOcaml { + /// The grammar for `ocaml`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\(\\*[\\s\\S]*?\\*\\)"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "'(?:[^\\\\\\r\\n']|\\\\(?:.|[ox]?[0-9a-f]{1,3}))'", + caseSensitive: false), + greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:\\\\(?:[\\s\\S]|\\r\\n)|[^\\\\\\r\\n\"])*\""), + greedy: true), + GrammarToken( + "string", compileHighlightPattern("\\{([a-z_]*)\\|[\\s\\S]*?\\|\\1\\}"), + greedy: true), + GrammarToken( + "number", + compileHighlightPattern("\\b(?:0b[01][01_]*|0o[0-7][0-7_]*)\\b", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[a-f0-9][a-f0-9_]*(?:\\.[a-f0-9_]*)?(?:p[+-]?\\d[\\d_]*)?(?!\\w)", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "\\b\\d[\\d_]*(?:\\.[\\d_]*)?(?:e[+-]?\\d[\\d_]*)?(?!\\w)", + caseSensitive: false)), + GrammarToken("directive", compileHighlightPattern("\\B#\\w+"), + alias: "property"), + GrammarToken("label", compileHighlightPattern("\\B~\\w+"), alias: "property"), + GrammarToken("type-variable", compileHighlightPattern("\\B'\\w+"), + alias: "function"), + GrammarToken("variant", compileHighlightPattern("`\\w+"), alias: "symbol"), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|match|method|module|mutable|new|nonrec|object|of|open|private|rec|sig|struct|then|to|try|type|val|value|virtual|when|where|while|with)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("operator-like-punctuation", + compileHighlightPattern("\\[[<>|]|[>|]\\]|\\{<|>\\}"), + alias: "punctuation"), + GrammarToken( + "operator", + compileHighlightPattern( + "\\.[.~]|:[=>]|[=<>@^|&+\\-*\\/\$%!?~][!\$%&*+\\-.\\/:<=>?@^|~]*|\\b(?:and|asr|land|lor|lsl|lsr|lxor|mod|or)\\b")), + GrammarToken("punctuation", + compileHighlightPattern(";;|::|[(){}\\[\\].,:;#]|\\b_\\b")), +]); diff --git a/lib/highlight/perl.dart b/lib/highlight/perl.dart new file mode 100644 index 0000000..483d5e3 --- /dev/null +++ b/lib/highlight/perl.dart @@ -0,0 +1,80 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `perl`. +/// +/// Import this library only when you need `perl` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightPerl { + /// The grammar for `perl`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^\\s*)=\\w[\\s\\S]*?=cut.*", multiLine: true), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\\$])#.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "\\b(?:q|qq|qw|qx)(?![a-zA-Z0-9])\\s*(?:([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1|([a-zA-Z0-9])(?:(?!\\2)[^\\\\]|\\\\[\\s\\S])*\\2|(?:\\((?:[^()\\\\]|\\\\[\\s\\S])*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\}|\\[(?:[^[\\]\\\\]|\\\\[\\s\\S])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>))"), + greedy: true), + GrammarToken("string", + compileHighlightPattern("(\"|`)(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1"), + greedy: true), + GrammarToken("string", compileHighlightPattern("'(?:[^'\\\\\\r\\n]|\\\\.)*'"), + greedy: true), + GrammarToken( + "regex", + compileHighlightPattern( + "\\b(?:m|qr)(?![a-zA-Z0-9])\\s*(?:([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1|([a-zA-Z0-9])(?:(?!\\2)[^\\\\]|\\\\[\\s\\S])*\\2|(?:\\((?:[^()\\\\]|\\\\[\\s\\S])*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\}|\\[(?:[^[\\]\\\\]|\\\\[\\s\\S])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>))[msixpodualngc]*"), + greedy: true), + GrammarToken( + "regex", + compileHighlightPattern( + "(^|[^-])\\b(?:s|tr|y)(?![a-zA-Z0-9])\\s*(?:([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\2)[^\\\\]|\\\\[\\s\\S])*\\2(?:(?!\\2)[^\\\\]|\\\\[\\s\\S])*\\2|([a-zA-Z0-9])(?:(?!\\3)[^\\\\]|\\\\[\\s\\S])*\\3(?:(?!\\3)[^\\\\]|\\\\[\\s\\S])*\\3|(?:\\((?:[^()\\\\]|\\\\[\\s\\S])*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\}|\\[(?:[^[\\]\\\\]|\\\\[\\s\\S])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>)\\s*(?:\\((?:[^()\\\\]|\\\\[\\s\\S])*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\}|\\[(?:[^[\\]\\\\]|\\\\[\\s\\S])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>))[msixpodualngcer]*"), + lookbehind: true, + greedy: true), + GrammarToken( + "regex", + compileHighlightPattern( + "\\/(?:[^\\/\\\\\\r\\n]|\\\\.)*\\/[msixpodualngc]*(?=\\s*(?:\$|[\\r\\n,.;})&|\\-+*~<>!?^]|(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|x|xor)\\b))"), + greedy: true), + GrammarToken("variable", compileHighlightPattern("[&*\$@%]\\{\\^[A-Z]+\\}")), + GrammarToken("variable", compileHighlightPattern("[&*\$@%]\\^[A-Z_]")), + GrammarToken("variable", compileHighlightPattern("[&*\$@%]#?(?=\\{)")), + GrammarToken( + "variable", + compileHighlightPattern( + "[&*\$@%]#?(?:(?:::)*'?(?!\\d)[\\w\$]+(?![\\w\$]))+(?:::)*")), + GrammarToken("variable", compileHighlightPattern("[&*\$@%]\\d+")), + GrammarToken( + "variable", + compileHighlightPattern( + "(?!%=)[\$@%][!\"#\$%&'()*+,\\-.\\/:;<=>?@[\\\\\\]^_`{|}~]")), + GrammarToken("filehandle", compileHighlightPattern("<(?![<=])\\S*?>|\\b_\\b"), + alias: "symbol"), + GrammarToken("v-string", + compileHighlightPattern("v\\d+(?:\\.\\d+)*|\\d+(?:\\.\\d+){2,}"), + alias: "string"), + GrammarToken("function", compileHighlightPattern("(\\bsub[ \\t]+)\\w+"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:any|break|continue|default|delete|die|do|else|elsif|eval|for|foreach|given|goto|if|last|local|my|next|our|package|print|redo|require|return|say|state|sub|switch|undef|unless|until|use|when|while)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:0x[\\dA-Fa-f](?:_?[\\dA-Fa-f])*|0b[01](?:_?[01])*|(?:(?:\\d(?:_?\\d)*)?\\.)?\\d(?:_?\\d)*(?:[Ee][+-]?\\d+)?)\\b")), + GrammarToken( + "operator", + compileHighlightPattern( + "-[rwxoRWXOezsfdlpSbctugkTBMAC]\\b|\\+[+=]?|-[-=>]?|\\*\\*?=?|\\/\\/?=?|=[=~>]?|~[~=]?|\\|\\|?=?|&&?=?|<(?:=>?|<=?)?|>>?=?|![~=]?|[%^]=?|\\.(?:=|\\.\\.?)?|[\\\\?]|\\bx(?:=|\\b)|\\b(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|xor)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),:]")), +]); diff --git a/lib/highlight/php.dart b/lib/highlight/php.dart new file mode 100644 index 0000000..f8638f1 --- /dev/null +++ b/lib/highlight/php.dart @@ -0,0 +1,429 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `php`. +/// +/// Import this library only when you need `php` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightPhp { + /// The grammar for `php`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "delimiter", + compileHighlightPattern("\\?>\$|^<\\?(?:php(?=\\s)|=)?", + caseSensitive: false), + alias: "important"), + GrammarToken("comment", + compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*|#(?!\\[).*")), + GrammarToken("string", + compileHighlightPattern("<<<'([^']+)'[\\r\\n](?:.*[\\r\\n])*?\\1;"), + greedy: true, alias: "nowdoc-string", inside: () => _g1), + GrammarToken( + "string", + compileHighlightPattern( + "<<<(?:\"([^\"]+)\"[\\r\\n](?:.*[\\r\\n])*?\\1;|([a-z_]\\w*)[\\r\\n](?:.*[\\r\\n])*?\\2;)", + caseSensitive: false), + greedy: true, + alias: "heredoc-string", + inside: () => _g3), + GrammarToken( + "string", compileHighlightPattern("`(?:\\\\[\\s\\S]|[^\\\\`])*`"), + greedy: true, alias: "backtick-quoted-string"), + GrammarToken( + "string", compileHighlightPattern("'(?:\\\\[\\s\\S]|[^\\\\'])*'"), + greedy: true, alias: "single-quoted-string"), + GrammarToken( + "string", compileHighlightPattern("\"(?:\\\\[\\s\\S]|[^\\\\\"])*\""), + greedy: true, alias: "double-quoted-string", inside: () => _g5), + GrammarToken( + "attribute", + compileHighlightPattern( + "#\\[(?:[^\"'\\/#]|\\/(?![*/])|\\/\\/.*\$|#(?!\\[).*\$|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*')+\\](?=\\s*[a-z\$#])", + caseSensitive: false, + multiLine: true), + greedy: true, + inside: () => _g6), + GrammarToken("variable", compileHighlightPattern("\\\$+(?:\\w+\\b|(?=\\{))")), + GrammarToken( + "package", + compileHighlightPattern( + "(namespace\\s+|use\\s+(?:function\\s+)?)(?:\\\\?\\b[a-z_]\\w*)+\\b(?!\\\\)", + caseSensitive: false), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "class-name-definition", + compileHighlightPattern( + "(\\b(?:class|enum|interface|trait)\\s+)\\b[a-z_]\\w*(?!\\\\)\\b", + caseSensitive: false), + lookbehind: true, + alias: "class-name"), + GrammarToken( + "function-definition", + compileHighlightPattern("(\\bfunction\\s+)[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false), + lookbehind: true, + alias: "function"), + GrammarToken( + "keyword", + compileHighlightPattern( + "(\\(\\s*)\\b(?:array|bool|boolean|float|int|integer|object|string)\\b(?=\\s*\\))", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "type-casting"), + GrammarToken( + "keyword", + compileHighlightPattern( + "([(,?]\\s*)\\b(?:array(?!\\s*\\()|bool|callable|(?:false|null)(?=\\s*\\|)|float|int|iterable|mixed|object|self|static|string)\\b(?=\\s*\\\$)", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "type-hint"), + GrammarToken( + "keyword", + compileHighlightPattern( + "(\\)\\s*:\\s*(?:\\?\\s*)?)\\b(?:array(?!\\s*\\()|bool|callable|(?:false|null)(?=\\s*\\|)|float|int|iterable|mixed|never|object|self|static|string|void)\\b", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "return-type"), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:array(?!\\s*\\()|bool|float|int|iterable|mixed|object|string|void)\\b", + caseSensitive: false), + greedy: true, + alias: "type-declaration"), + GrammarToken( + "keyword", + compileHighlightPattern( + "(\\|\\s*)(?:false|null)\\b|\\b(?:false|null)(?=\\s*\\|)", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "type-declaration"), + GrammarToken( + "keyword", + compileHighlightPattern("\\b(?:parent|self|static)(?=\\s*::)", + caseSensitive: false), + greedy: true, + alias: "static-context"), + GrammarToken("keyword", + compileHighlightPattern("(\\byield\\s+)from\\b", caseSensitive: false), + lookbehind: true), + GrammarToken( + "keyword", compileHighlightPattern("\\bclass\\b", caseSensitive: false)), + GrammarToken( + "keyword", + compileHighlightPattern( + "((?:^|[^\\s>:]|(?:^|[^-])>|(?:^|[^:]):)\\s*)\\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\\b", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "argument-name", + compileHighlightPattern("([(,]\\s*)\\b[a-z_]\\w*(?=\\s*:(?!:))", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:extends|implements|instanceof|new(?!\\s+self|\\s+static))\\s+|\\bcatch\\s*\\()\\b[a-z_]\\w*(?!\\\\)\\b", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern("(\\|\\s*)\\b[a-z_]\\w*(?!\\\\)\\b", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern("\\b[a-z_]\\w*(?!\\\\)\\b(?=\\s*\\|)", + caseSensitive: false), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern("(\\|\\s*)(?:\\\\?\\b[a-z_]\\w*)+\\b", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g10), + GrammarToken( + "class-name", + compileHighlightPattern("(?:\\\\?\\b[a-z_]\\w*)+\\b(?=\\s*\\|)", + caseSensitive: false), + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g11), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:extends|implements|instanceof|new(?!\\s+self\\b|\\s+static\\b))\\s+|\\bcatch\\s*\\()(?:\\\\?\\b[a-z_]\\w*)+\\b(?!\\\\)", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g12), + GrammarToken( + "class-name", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\\$)", + caseSensitive: false), + greedy: true, + alias: "type-declaration"), + GrammarToken( + "class-name", + compileHighlightPattern("(?:\\\\?\\b[a-z_]\\w*)+(?=\\s*\\\$)", + caseSensitive: false), + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g13), + GrammarToken("class-name", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*::)", caseSensitive: false), + greedy: true, alias: "static-context"), + GrammarToken( + "class-name", + compileHighlightPattern("(?:\\\\?\\b[a-z_]\\w*)+(?=\\s*::)", + caseSensitive: false), + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g14), + GrammarToken( + "class-name", + compileHighlightPattern("([(,?]\\s*)[a-z_]\\w*(?=\\s*\\\$)", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "type-hint"), + GrammarToken( + "class-name", + compileHighlightPattern("([(,?]\\s*)(?:\\\\?\\b[a-z_]\\w*)+(?=\\s*\\\$)", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g15), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\)\\s*:\\s*(?:\\?\\s*)?)\\b[a-z_]\\w*(?!\\\\)\\b", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "return-type"), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\)\\s*:\\s*(?:\\?\\s*)?)(?:\\\\?\\b[a-z_]\\w*)+\\b(?!\\\\)", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "class-name-fully-qualified", + inside: () => _g16), + GrammarToken("constant", + compileHighlightPattern("\\b(?:false|true)\\b", caseSensitive: false), + alias: "boolean"), + GrammarToken( + "constant", + compileHighlightPattern("(::\\s*)\\b[a-z_]\\w*\\b(?!\\s*\\()", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken( + "constant", + compileHighlightPattern( + "(\\b(?:case|const)\\s+)\\b[a-z_]\\w*(?=\\s*[;=])", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken("constant", + compileHighlightPattern("\\b(?:null)\\b", caseSensitive: false)), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z_][A-Z0-9_]*\\b(?!\\s*\\()")), + GrammarToken( + "function", + compileHighlightPattern( + "(^|[^\\\\\\w])\\\\?[a-z_](?:[\\w\\\\]*\\w)?(?=\\s*\\()", + caseSensitive: false), + lookbehind: true, + inside: () => _g17), + GrammarToken("property", compileHighlightPattern("(->\\s*)\\w+"), + lookbehind: true), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0b[01]+(?:_[01]+)*\\b|\\b0o[0-7]+(?:_[0-7]+)*\\b|\\b0x[\\da-f]+(?:_[\\da-f]+)*\\b|(?:\\b\\d+(?:_\\d+)*\\.?(?:\\d+(?:_\\d+)*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "|\\?\\?=?|\\.{3}|\\??->|[!=]=?=?|::|\\*\\*=?|--|\\+\\+|&&|\\|\\||<<|>>|[?~]|[/^|%*&<>.+-]=?")), + GrammarToken("punctuation", compileHighlightPattern("[{}\\[\\](),:;]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "delimiter", + compileHighlightPattern("^<<<'[^']+'|[a-z_]\\w*;\$", + caseSensitive: false), + alias: "symbol", + inside: () => _g2), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<<<'?|[';]\$")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "delimiter", + compileHighlightPattern("^<<<(?:\"[^\"]+\"|[a-z_]\\w*)|[a-z_]\\w*;\$", + caseSensitive: false), + alias: "symbol", + inside: () => _g4), + GrammarToken( + "interpolation", + compileHighlightPattern( + "\\{\\\$(?:\\{(?:\\{[^{}]+\\}|[^{}]+)\\}|[^{}])+\\}|(^|[^\\\\{])\\\$+(?:\\w+(?:\\[[^\\r\\n\\[\\]]+\\]|->\\w+)?)"), + lookbehind: true, + inside: () => _g0), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<<<\"?|[\";]\$")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "\\{\\\$(?:\\{(?:\\{[^{}]+\\}|[^{}]+)\\}|[^{}])+\\}|(^|[^\\\\{])\\\$+(?:\\w+(?:\\[[^\\r\\n\\[\\]]+\\]|->\\w+)?)"), + lookbehind: true, + inside: () => _g0), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "attribute-content", compileHighlightPattern("^(#\\[)[\\s\\S]+(?=\\]\$)"), + lookbehind: true, inside: () => _g7), + GrammarToken("delimiter", compileHighlightPattern("^#\\[|\\]\$"), + alias: "punctuation"), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*|#(?!\\[).*")), + GrammarToken("string", + compileHighlightPattern("<<<'([^']+)'[\\r\\n](?:.*[\\r\\n])*?\\1;"), + greedy: true, alias: "nowdoc-string", inside: () => _g1), + GrammarToken( + "string", + compileHighlightPattern( + "<<<(?:\"([^\"]+)\"[\\r\\n](?:.*[\\r\\n])*?\\1;|([a-z_]\\w*)[\\r\\n](?:.*[\\r\\n])*?\\2;)", + caseSensitive: false), + greedy: true, + alias: "heredoc-string", + inside: () => _g3), + GrammarToken( + "string", compileHighlightPattern("`(?:\\\\[\\s\\S]|[^\\\\`])*`"), + greedy: true, alias: "backtick-quoted-string"), + GrammarToken( + "string", compileHighlightPattern("'(?:\\\\[\\s\\S]|[^\\\\'])*'"), + greedy: true, alias: "single-quoted-string"), + GrammarToken( + "string", compileHighlightPattern("\"(?:\\\\[\\s\\S]|[^\\\\\"])*\""), + greedy: true, alias: "double-quoted-string", inside: () => _g5), + GrammarToken( + "attribute-class-name", + compileHighlightPattern("([^:]|^)\\b[a-z_]\\w*(?!\\\\)\\b", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "class-name"), + GrammarToken( + "attribute-class-name", + compileHighlightPattern("([^:]|^)(?:\\\\?\\b[a-z_]\\w*)+", + caseSensitive: false), + lookbehind: true, + greedy: true, + alias: "class-name", + inside: () => _g8), + GrammarToken("constant", + compileHighlightPattern("\\b(?:false|true)\\b", caseSensitive: false), + alias: "boolean"), + GrammarToken( + "constant", + compileHighlightPattern("(::\\s*)\\b[a-z_]\\w*\\b(?!\\s*\\()", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken( + "constant", + compileHighlightPattern( + "(\\b(?:case|const)\\s+)\\b[a-z_]\\w*(?=\\s*[;=])", + caseSensitive: false), + lookbehind: true, + greedy: true), + GrammarToken("constant", + compileHighlightPattern("\\b(?:null)\\b", caseSensitive: false)), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z_][A-Z0-9_]*\\b(?!\\s*\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0b[01]+(?:_[01]+)*\\b|\\b0o[0-7]+(?:_[0-7]+)*\\b|\\b0x[\\da-f]+(?:_[\\da-f]+)*\\b|(?:\\b\\d+(?:_\\d+)*\\.?(?:\\d+(?:_\\d+)*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "|\\?\\?=?|\\.{3}|\\??->|[!=]=?=?|::|\\*\\*=?|--|\\+\\+|&&|\\|\\||<<|>>|[?~]|[/^|%*&<>.+-]=?")), + GrammarToken("punctuation", compileHighlightPattern("[{}\\[\\](),:;]")), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g9 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g11 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g12 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g13 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g14 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g15 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g16 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); + +final Grammar _g17 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\\\")), +]); diff --git a/lib/highlight/plain.dart b/lib/highlight/plain.dart new file mode 100644 index 0000000..2465066 --- /dev/null +++ b/lib/highlight/plain.dart @@ -0,0 +1,16 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `plain`. +/// +/// Import this library only when you need `plain` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightPlain { + /// The grammar for `plain`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([]); diff --git a/lib/highlight/powershell.dart b/lib/highlight/powershell.dart new file mode 100644 index 0000000..6c3bdd6 --- /dev/null +++ b/lib/highlight/powershell.dart @@ -0,0 +1,67 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `powershell`. +/// +/// Import this library only when you need `powershell` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightPowershell { + /// The grammar for `powershell`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("(^|[^`])<#[\\s\\S]*?#>"), + lookbehind: true), + GrammarToken("comment", compileHighlightPattern("(^|[^`])#.*"), + lookbehind: true), + GrammarToken("string", compileHighlightPattern("\"(?:`[\\s\\S]|[^`\"])*\""), + greedy: true, inside: () => _g1), + GrammarToken("string", compileHighlightPattern("'(?:[^']|'')*'"), + greedy: true), + GrammarToken( + "namespace", + compileHighlightPattern( + "\\[[a-z](?:\\[(?:\\[[^\\]]*\\]|[^\\[\\]])*\\]|[^\\[\\]])*\\]", + caseSensitive: false)), + GrammarToken("boolean", + compileHighlightPattern("\\\$(?:false|true)\\b", caseSensitive: false)), + GrammarToken("variable", compileHighlightPattern("\\\$\\w+\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "\\b(?:Add|Approve|Assert|Backup|Block|Checkpoint|Clear|Close|Compare|Complete|Compress|Confirm|Connect|Convert|ConvertFrom|ConvertTo|Copy|Debug|Deny|Disable|Disconnect|Dismount|Edit|Enable|Enter|Exit|Expand|Export|Find|ForEach|Format|Get|Grant|Group|Hide|Import|Initialize|Install|Invoke|Join|Limit|Lock|Measure|Merge|Move|New|Open|Optimize|Out|Ping|Pop|Protect|Publish|Push|Read|Receive|Redo|Register|Remove|Rename|Repair|Request|Reset|Resize|Resolve|Restart|Restore|Resume|Revoke|Save|Search|Select|Send|Set|Show|Skip|Sort|Split|Start|Step|Stop|Submit|Suspend|Switch|Sync|Tee|Test|Trace|Unblock|Undo|Uninstall|Unlock|Unprotect|Unpublish|Unregister|Update|Use|Wait|Watch|Where|Write)-[a-z]+\\b", + caseSensitive: false)), + GrammarToken( + "function", + compileHighlightPattern( + "\\b(?:ac|cat|chdir|clc|cli|clp|clv|compare|copy|cp|cpi|cpp|cvpa|dbp|del|diff|dir|ebp|echo|epal|epcsv|epsn|erase|fc|fl|ft|fw|gal|gbp|gc|gci|gcs|gdr|gi|gl|gm|gp|gps|group|gsv|gu|gv|gwmi|iex|ii|ipal|ipcsv|ipsn|irm|iwmi|iwr|kill|lp|ls|measure|mi|mount|move|mp|mv|nal|ndr|ni|nv|ogv|popd|ps|pushd|pwd|rbp|rd|rdr|ren|ri|rm|rmdir|rni|rnp|rp|rv|rvpa|rwmi|sal|saps|sasv|sbp|sc|select|set|shcm|si|sl|sleep|sls|sort|sp|spps|spsv|start|sv|swmi|tee|trcm|type|write)\\b", + caseSensitive: false)), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:Begin|Break|Catch|Class|Continue|Data|Define|Do|DynamicParam|Else|ElseIf|End|Exit|Filter|Finally|For|ForEach|From|Function|If|InlineScript|Parallel|Param|Process|Return|Sequence|Switch|Throw|Trap|Try|Until|Using|Var|While|Workflow)\\b", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "(^|\\W)(?:!|-(?:b?(?:and|x?or)|as|(?:Not)?(?:Contains|In|Like|Match)|eq|ge|gt|is(?:Not)?|Join|le|lt|ne|not|Replace|sh[lr])\\b|-[-=]?|\\+[+=]?|[*\\/%]=?)", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[|{}[\\];(),.]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "function", + compileHighlightPattern( + "(^|[^`])\\\$\\((?:\\\$\\([^\\r\\n()]*\\)|(?!\\\$\\()[^\\r\\n)])*\\)"), + lookbehind: true, + inside: () => _g0), + GrammarToken("boolean", + compileHighlightPattern("\\\$(?:false|true)\\b", caseSensitive: false)), + GrammarToken("variable", compileHighlightPattern("\\\$\\w+\\b")), +]); diff --git a/lib/highlight/protobuf.dart b/lib/highlight/protobuf.dart new file mode 100644 index 0000000..0087eb6 --- /dev/null +++ b/lib/highlight/protobuf.dart @@ -0,0 +1,91 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `protobuf`. +/// +/// Import this library only when you need `protobuf` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightProtobuf { + /// The grammar for `protobuf`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:enum|extend|message|service)\\s+)[A-Za-z_]\\w*(?=\\s*\\{)"), + lookbehind: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:rpc\\s+\\w+|returns)\\s*\\(\\s*(?:stream\\s+)?)\\.?[A-Za-z_]\\w*(?:\\.[A-Za-z_]\\w*)*(?=\\s*\\))"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:enum|extend|extensions|import|message|oneof|option|optional|package|public|repeated|required|reserved|returns|rpc(?=\\s+\\w)|service|stream|syntax|to)\\b(?!\\s*=\\s*\\d)")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\da-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken( + "map", + compileHighlightPattern( + "\\bmap<\\s*[\\w.]+\\s*,\\s*[\\w.]+\\s*>(?=\\s+[a-z_]\\w*\\s*[=;])", + caseSensitive: false), + alias: "class-name", + inside: () => _g1), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:bool|bytes|double|s?fixed(?:32|64)|float|[su]?int(?:32|64)|string)\\b")), + GrammarToken( + "positional-class-name", + compileHighlightPattern( + "(?:\\b|\\B\\.)[a-z_]\\w*(?:\\.[a-z_]\\w*)*(?=\\s+[a-z_]\\w*\\s*[=;])", + caseSensitive: false), + alias: "class-name", + inside: () => _g2), + GrammarToken( + "annotation", + compileHighlightPattern("(\\[\\s*)[a-z_]\\w*(?=\\s*=)", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "[<>]=?|[!=]=?=?|--?|\\+\\+?|&&?|\\|\\|?|[?*/~^%]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[<>.,]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:bool|bytes|double|s?fixed(?:32|64)|float|[su]?int(?:32|64)|string)\\b")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/python.dart b/lib/highlight/python.dart new file mode 100644 index 0000000..dda6687 --- /dev/null +++ b/lib/highlight/python.dart @@ -0,0 +1,87 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `python`. +/// +/// Import this library only when you need `python` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightPython { + /// The grammar for `python`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\])#.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string-interpolation", + compileHighlightPattern( + "(?:f|fr|rf)(?:(\"\"\"|''')[\\s\\S]*?\\1|(\"|')(?:\\\\.|(?!\\2)[^\\\\\\r\\n])*\\2)", + caseSensitive: false), + greedy: true, + inside: () => _g1), + GrammarToken( + "triple-quoted-string", + compileHighlightPattern("(?:[rub]|br|rb)?(\"\"\"|''')[\\s\\S]*?\\1", + caseSensitive: false), + greedy: true, + alias: "string"), + GrammarToken( + "string", + compileHighlightPattern( + "(?:[rub]|br|rb)?(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1", + caseSensitive: false), + greedy: true), + GrammarToken("function", + compileHighlightPattern("((?:^|\\s)def[ \\t]+)[a-zA-Z_]\\w*(?=\\s*\\()"), + lookbehind: true), + GrammarToken("class-name", + compileHighlightPattern("(\\bclass\\s+)\\w+", caseSensitive: false), + lookbehind: true), + GrammarToken("decorator", + compileHighlightPattern("(^[\\t ]*)@\\w+(?:\\.\\w+)*", multiLine: true), + lookbehind: true, alias: "annotation", inside: () => _g3), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:_(?=\\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\\b")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:False|None|True)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\\b|(?:\\b\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\B\\.\\d+(?:_\\d+)*)(?:e[+-]?\\d+(?:_\\d+)*)?j?(?!\\w)", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "[-+%=]=?|!=|:=|\\*\\*?=?|\\/\\/?=?|<[<=>]?|>[=>]?|[&|^~]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^{])(?:\\{\\{)*)\\{(?!\\{)(?:[^{}]|\\{(?!\\{)(?:[^{}]|\\{(?!\\{)(?:[^{}])+\\})+\\})+\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("format-spec", compileHighlightPattern("(:)[^:(){}]+(?=\\}\$)"), + lookbehind: true), + GrammarToken("conversion-option", compileHighlightPattern("![sra](?=[:}]\$)"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/r.dart b/lib/highlight/r.dart new file mode 100644 index 0000000..1337b89 --- /dev/null +++ b/lib/highlight/r.dart @@ -0,0 +1,39 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `r`. +/// +/// Import this library only when you need `r` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightR { + /// The grammar for `r`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("#.*")), + GrammarToken("string", + compileHighlightPattern("(['\"])(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken("percent-operator", compileHighlightPattern("%[^%\\s]*%"), + alias: "operator"), + GrammarToken("boolean", compileHighlightPattern("\\b(?:FALSE|TRUE)\\b")), + GrammarToken("ellipsis", compileHighlightPattern("\\.\\.(?:\\.|\\d+)")), + GrammarToken("number", compileHighlightPattern("\\b(?:Inf|NaN)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0x[\\dA-Fa-f]+(?:\\.\\d*)?|\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:[EePp][+-]?\\d+)?[iL]?")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:NA|NA_character_|NA_complex_|NA_integer_|NA_real_|NULL|break|else|for|function|if|in|next|repeat|while)\\b")), + GrammarToken( + "operator", + compileHighlightPattern( + "->?>?|<(?:=|=!]=?|::?|&&?|\\|\\|?|[+*\\/^\$@~]")), + GrammarToken("punctuation", compileHighlightPattern("[(){}\\[\\],;]")), +]); diff --git a/lib/highlight/regex.dart b/lib/highlight/regex.dart new file mode 100644 index 0000000..9948974 --- /dev/null +++ b/lib/highlight/regex.dart @@ -0,0 +1,96 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `regex`. +/// +/// Import this library only when you need `regex` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightRegex { + /// The grammar for `regex`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g1), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g3), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g4), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g2), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); diff --git a/lib/highlight/ruby.dart b/lib/highlight/ruby.dart new file mode 100644 index 0000000..c97078e --- /dev/null +++ b/lib/highlight/ruby.dart @@ -0,0 +1,233 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `ruby`. +/// +/// Import this library only when you need `ruby` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightRuby { + /// The grammar for `ruby`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("#.*|^=begin\\s[\\s\\S]*?^=end", multiLine: true), + greedy: true), + GrammarToken( + "string-literal", + compileHighlightPattern( + "%[qQiIwWs]?(?:([^a-zA-Z0-9\\s{(\\[<=])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1|\\((?:[^()\\\\]|\\\\[\\s\\S]|\\((?:[^()\\\\]|\\\\[\\s\\S])*\\))*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S]|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\})*\\}|\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S]|\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S])*\\])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>)*>)"), + greedy: true, + inside: () => _g1), + GrammarToken( + "string-literal", + compileHighlightPattern( + "(\"|')(?:#\\{[^}]+\\}|#(?!\\{)|\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\#\\r\\n])*\\1"), + greedy: true, + inside: () => _g3), + GrammarToken( + "string-literal", + compileHighlightPattern( + "<<[-~]?([a-z_]\\w*)[\\r\\n](?:.*[\\r\\n])*?[\\t ]*\\1", + caseSensitive: false), + greedy: true, + alias: "heredoc-string", + inside: () => _g4), + GrammarToken( + "string-literal", + compileHighlightPattern( + "<<[-~]?'([a-z_]\\w*)'[\\r\\n](?:.*[\\r\\n])*?[\\t ]*\\1", + caseSensitive: false), + greedy: true, + alias: "heredoc-string", + inside: () => _g6), + GrammarToken( + "command-literal", + compileHighlightPattern( + "%x(?:([^a-zA-Z0-9\\s{(\\[<=])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1|\\((?:[^()\\\\]|\\\\[\\s\\S]|\\((?:[^()\\\\]|\\\\[\\s\\S])*\\))*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S]|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\})*\\}|\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S]|\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S])*\\])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>)*>)"), + greedy: true, + inside: () => _g8), + GrammarToken( + "command-literal", + compileHighlightPattern( + "`(?:#\\{[^}]+\\}|#(?!\\{)|\\\\(?:\\r\\n|[\\s\\S])|[^\\\\`#\\r\\n])*`"), + greedy: true, + inside: () => _g9), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|module)\\s+|\\bcatch\\s+\\()[\\w.\\\\]+|\\b[A-Z_]\\w*(?=\\s*\\.\\s*new\\b)"), + lookbehind: true, + inside: () => _g10), + GrammarToken( + "regex-literal", + compileHighlightPattern( + "%r(?:([^a-zA-Z0-9\\s{(\\[<=])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1|\\((?:[^()\\\\]|\\\\[\\s\\S]|\\((?:[^()\\\\]|\\\\[\\s\\S])*\\))*\\)|\\{(?:[^{}\\\\]|\\\\[\\s\\S]|\\{(?:[^{}\\\\]|\\\\[\\s\\S])*\\})*\\}|\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S]|\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S])*\\])*\\]|<(?:[^<>\\\\]|\\\\[\\s\\S]|<(?:[^<>\\\\]|\\\\[\\s\\S])*>)*>)[egimnosux]{0,6}"), + greedy: true, + inside: () => _g11), + GrammarToken( + "regex-literal", + compileHighlightPattern( + "(^|[^/])\\/(?!\\/)(?:\\[[^\\r\\n\\]]+\\]|\\\\.|[^[/\\\\\\r\\n])+\\/[egimnosux]{0,6}(?=\\s*(?:\$|[\\r\\n,.;})#]))"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken( + "variable", compileHighlightPattern("[@\$]+[a-zA-Z_]\\w*(?:[?!]|\\b)")), + GrammarToken( + "symbol", + compileHighlightPattern( + "(^|[^:]):(?:\"(?:\\\\.|[^\"\\\\\\r\\n])*\"|(?:\\b[a-zA-Z_]\\w*|[^\\s\\0-\\x7F]+)[?!]?|\\\$.)"), + lookbehind: true, + greedy: true), + GrammarToken( + "symbol", + compileHighlightPattern( + "([\\r\\n{(,][ \\t]*)(?:\"(?:\\\\.|[^\"\\\\\\r\\n])*\"|(?:\\b[a-zA-Z_]\\w*|[^\\s\\0-\\x7F]+)[?!]?|\\\$.)(?=:(?!:))"), + lookbehind: true, + greedy: true), + GrammarToken("method-definition", + compileHighlightPattern("(\\bdef\\s+)\\w+(?:\\s*\\.\\s*\\w+)?"), + lookbehind: true, inside: () => _g13), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\\b")), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z][A-Z0-9_]*(?:[?!]|\\b)")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\da-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken("double-colon", compileHighlightPattern("::"), + alias: "punctuation"), + GrammarToken( + "operator", + compileHighlightPattern( + "\\.{2,3}|&\\.|===||[!=]?~|(?:&&|\\|\\||<<|>>|\\*\\*|[+\\-*/%<>!^&|=])=?|[?:]")), + GrammarToken("punctuation", compileHighlightPattern("[(){}[\\].,;]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("content", compileHighlightPattern("^(#\\{)[\\s\\S]+(?=\\}\$)"), + lookbehind: true, inside: () => _g0), + GrammarToken("delimiter", compileHighlightPattern("^#\\{|\\}\$"), + alias: "punctuation"), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "delimiter", + compileHighlightPattern("^<<[-~]?[a-z_]\\w*|\\b[a-z_]\\w*\$", + caseSensitive: false), + inside: () => _g5), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("symbol", compileHighlightPattern("\\b\\w+")), + GrammarToken("punctuation", compileHighlightPattern("^<<[-~]?")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "delimiter", + compileHighlightPattern("^<<[-~]?'[a-z_]\\w*'|\\b[a-z_]\\w*\$", + caseSensitive: false), + inside: () => _g7), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken("symbol", compileHighlightPattern("\\b\\w+")), + GrammarToken("punctuation", compileHighlightPattern("^<<[-~]?'|'\$")), +]); + +final Grammar _g8 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("command", compileHighlightPattern("[\\s\\S]+"), + alias: "string"), +]); + +final Grammar _g9 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("command", compileHighlightPattern("[\\s\\S]+"), + alias: "string"), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g11 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("regex", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g12 = Grammar([ + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)#\\{(?:[^{}]|\\{[^{}]*\\})*\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("regex", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g13 = Grammar([ + GrammarToken("function", compileHighlightPattern("\\b\\w+\$")), + GrammarToken("keyword", compileHighlightPattern("^self\\b")), + GrammarToken("class-name", compileHighlightPattern("^\\w+")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/rust.dart b/lib/highlight/rust.dart new file mode 100644 index 0000000..0cc8d43 --- /dev/null +++ b/lib/highlight/rust.dart @@ -0,0 +1,121 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `rust`. +/// +/// Import this library only when you need `rust` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightRust { + /// The grammar for `rust`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "(^|[^\\\\])\\/\\*(?:[^*/]|\\*(?!\\/)|\\/(?!\\*)|\\/\\*(?:[^*/]|\\*(?!\\/)|\\/(?!\\*)|\\/\\*(?:[^*/]|\\*(?!\\/)|\\/(?!\\*)|\\/\\*(?:[^*/]|\\*(?!\\/)|\\/(?!\\*)|[^\\s\\S])*\\*\\/)*\\*\\/)*\\*\\/)*\\*\\/"), + lookbehind: true, + greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "b?\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|b?r(#*)\"(?:[^\"]|\"(?!\\1))*\"\\1"), + greedy: true), + GrammarToken( + "char", + compileHighlightPattern( + "b?'(?:\\\\(?:x[0-7][\\da-fA-F]|u\\{(?:[\\da-fA-F]_*){1,6}\\}|.)|[^\\\\\\r\\n\\t'])'"), + greedy: true), + GrammarToken( + "attribute", + compileHighlightPattern( + "#!?\\[(?:[^\\[\\]\"]|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")*\\]"), + greedy: true, + alias: "attr-name", + inside: () => _g1), + GrammarToken( + "closure-params", + compileHighlightPattern( + "([=(,:]\\s*|\\bmove\\s*)\\|[^|]*\\||\\|[^|]*\\|(?=\\s*(?:\\{|->))"), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken("lifetime-annotation", compileHighlightPattern("'\\w+"), + alias: "symbol"), + GrammarToken( + "fragment-specifier", compileHighlightPattern("(\\\$\\w+:)[a-z]+"), + lookbehind: true, alias: "punctuation"), + GrammarToken("variable", compileHighlightPattern("\\\$\\w+")), + GrammarToken( + "function-definition", compileHighlightPattern("(\\bfn\\s+)\\w+"), + lookbehind: true, alias: "function"), + GrammarToken("type-definition", + compileHighlightPattern("(\\b(?:enum|struct|trait|type|union)\\s+)\\w+"), + lookbehind: true, alias: "class-name"), + GrammarToken("module-declaration", + compileHighlightPattern("(\\b(?:crate|mod)\\s+)[a-z][a-z_\\d]*"), + lookbehind: true, alias: "namespace"), + GrammarToken( + "module-declaration", + compileHighlightPattern( + "(\\b(?:crate|self|super)\\s*)::\\s*[a-z][a-z_\\d]*\\b(?:\\s*::(?:\\s*[a-z][a-z_\\d]*\\s*::)*)?"), + lookbehind: true, + alias: "namespace", + inside: () => _g3), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:Self|abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:bool|char|f(?:32|64)|[ui](?:8|16|32|64|128|size)|str)\\b")), + GrammarToken("function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*(?:::\\s*<|\\())")), + GrammarToken("macro", compileHighlightPattern("\\b\\w+!"), alias: "property"), + GrammarToken("constant", compileHighlightPattern("\\b[A-Z_][A-Z_\\d]+\\b")), + GrammarToken("class-name", compileHighlightPattern("\\b[A-Z]\\w*\\b")), + GrammarToken( + "namespace", + compileHighlightPattern( + "(?:\\b[a-z][a-z_\\d]*\\s*::\\s*)*\\b[a-z][a-z_\\d]*\\s*::(?!\\s*<)"), + inside: () => _g4), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:0x[\\dA-Fa-f](?:_?[\\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:(?:\\d(?:_?\\d)*)?\\.)?\\d(?:_?\\d)*(?:[Ee][+-]?\\d+)?)(?:_?(?:f32|f64|[iu](?:8|16|32|64|size)?))?\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("punctuation", + compileHighlightPattern("->|\\.\\.=|\\.{1,3}|::|[{}[\\];(),:]")), + GrammarToken( + "operator", + compileHighlightPattern( + "[-+*\\/%!^]=?|=[=>]?|&[&=]?|\\|[|=]?|<>?=?|[@?]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken( + "string", + compileHighlightPattern( + "b?\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|b?r(#*)\"(?:[^\"]|\"(?!\\1))*\"\\1"), + greedy: true), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("closure-punctuation", compileHighlightPattern("^\\||\\|\$"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("::")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("::")), +]); diff --git a/lib/highlight/sass.dart b/lib/highlight/sass.dart new file mode 100644 index 0000000..907b466 --- /dev/null +++ b/lib/highlight/sass.dart @@ -0,0 +1,101 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `sass`. +/// +/// Import this library only when you need `sass` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightSass { + /// The grammar for `sass`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "^([ \\t]*)\\/[\\/*].*(?:(?:\\r?\\n|\\r)\\1[ \\t].+)*", + multiLine: true), + lookbehind: true, + greedy: true), + GrammarToken("atrule-line", + compileHighlightPattern("^(?:[ \\t]*)[@+=].+", multiLine: true), + greedy: true, inside: () => _g1), + GrammarToken( + "url", + compileHighlightPattern( + "\\burl\\((?:(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')|(?:[^\\\\\\r\\n()\"']|\\\\[\\s\\S])*)\\)", + caseSensitive: false), + greedy: true, + inside: () => _g2), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken("variable-line", + compileHighlightPattern("^[ \\t]*\\\$.+", multiLine: true), + greedy: true, inside: () => _g3), + GrammarToken( + "property-line", + compileHighlightPattern("^[ \\t]*(?:[^:\\s]+ *:.*|:[^:\\s].*)", + multiLine: true), + greedy: true, + inside: () => _g4), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken( + "selector", + compileHighlightPattern( + "^([ \\t]*)\\S(?:,[^,\\r\\n]+|[^,\\r\\n]*)(?:,[^,\\r\\n]+)*(?:,(?:\\r?\\n|\\r)\\1[ \\t]+\\S(?:,[^,\\r\\n]+|[^,\\r\\n]*)(?:,[^,\\r\\n]+)*)*", + multiLine: true), + lookbehind: true, + greedy: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("atrule", compileHighlightPattern("(?:@[\\w-]+|[+=])")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^url", caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("^\\(|\\)\$")), + GrammarToken( + "string", + compileHighlightPattern( + "^(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')\$"), + alias: "url"), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern(":")), + GrammarToken( + "variable", compileHighlightPattern("\\\$[-\\w]+|#\\{\\\$[-\\w]+\\}")), + GrammarToken("operator", + compileHighlightPattern("[+*\\/%]|[=!]=|<=?|>=?|\\b(?:and|not|or)\\b")), + GrammarToken("operator", compileHighlightPattern("(\\s)-(?=\\s)"), + lookbehind: true), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("property", compileHighlightPattern("[^:\\s]+(?=\\s*:)")), + GrammarToken("property", compileHighlightPattern("(:)[^:\\s]+"), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern(":")), + GrammarToken( + "variable", compileHighlightPattern("\\\$[-\\w]+|#\\{\\\$[-\\w]+\\}")), + GrammarToken("operator", + compileHighlightPattern("[+*\\/%]|[=!]=|<=?|>=?|\\b(?:and|not|or)\\b")), + GrammarToken("operator", compileHighlightPattern("(\\s)-(?=\\s)"), + lookbehind: true), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), +]); diff --git a/lib/highlight/scala.dart b/lib/highlight/scala.dart new file mode 100644 index 0000000..517681f --- /dev/null +++ b/lib/highlight/scala.dart @@ -0,0 +1,159 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `scala`. +/// +/// Import this library only when you need `scala` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightScala { + /// The grammar for `scala`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string-interpolation", + compileHighlightPattern( + "\\b[a-z]\\w*(?:\"\"\"(?:[^\$]|\\\$(?:[^{]|\\{(?:[^{}]|\\{[^{}]*\\})*\\}))*?\"\"\"|\"(?:[^\$\"\\r\\n]|\\\$(?:[^{]|\\{(?:[^{}]|\\{[^{}]*\\})*\\}))*\")", + caseSensitive: false), + greedy: true, + inside: () => _g1), + GrammarToken( + "triple-quoted-string", compileHighlightPattern("\"\"\"[\\s\\S]*?\"\"\""), + greedy: true, alias: "string"), + GrammarToken( + "char", compileHighlightPattern("'(?:\\\\.|[^'\\\\\\r\\n]){1,6}'"), + greedy: true), + GrammarToken("string", + compileHighlightPattern("(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken("annotation", + compileHighlightPattern("(^|[^.])@\\w+(?:\\s*\\.\\s*\\w+)*"), + lookbehind: true, alias: "punctuation"), + GrammarToken( + "generics", + compileHighlightPattern( + "<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&)|<(?:[\\w\\s,.?]|&(?!&))*>)*>)*>)*>"), + inside: () => _g3), + GrammarToken( + "import", + compileHighlightPattern( + "(\\bimport\\s+)(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*|\\*)(?=\\s*;)"), + lookbehind: true, + inside: () => _g6), + GrammarToken( + "import", + compileHighlightPattern( + "(\\bimport\\s+static\\s+)(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*(?:\\w+|\\*)(?=\\s*;)"), + lookbehind: true, + alias: "static", + inside: () => _g7), + GrammarToken( + "namespace", + compileHighlightPattern( + "(\\b(?:exports|import(?:\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\s+)(?!\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\\s*[(){}[\\]<>=%~.:,;?+\\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b)[a-z]\\w*(?:\\.[a-z]\\w*)*\\.?"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "keyword", + compileHighlightPattern( + "<-|=>|\\b(?:abstract|case|catch|class|def|derives|do|else|enum|extends|extension|final|finally|for|forSome|given|if|implicit|import|infix|inline|lazy|match|new|null|object|opaque|open|override|package|private|protected|return|sealed|self|super|this|throw|trait|transparent|try|type|using|val|var|while|with|yield)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x(?:[\\da-f]*\\.)?[\\da-f]+|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e\\d+)?[dfl]?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "(^|[^.])(?:<<=?|>>>?=?|->|--|\\+\\+|&&|\\|\\||::|[?:~]|[-+*/%&|^!=<>]=?)", + multiLine: true), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Any|AnyRef|AnyVal|Boolean|Byte|Char|Double|Float|Int|Long|Nothing|Short|String|Unit)\\b")), + GrammarToken("symbol", compileHighlightPattern("'[^\\d\\s\\\\]\\w*")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("id", compileHighlightPattern("^\\w+"), + greedy: true, alias: "function"), + GrammarToken("escape", compileHighlightPattern("\\\\\\\$\"|\\\$[\$\"]"), + greedy: true, alias: "symbol"), + GrammarToken("interpolation", + compileHighlightPattern("\\\$(?:\\w+|\\{(?:[^{}]|\\{[^{}]*\\})*\\})"), + greedy: true, inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^\\\$\\{?|\\}\$")), + GrammarToken("expression", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g0), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\\w.])(?:[a-z]\\w*\\s*\\.\\s*)*(?:[A-Z]\\w*\\s*\\.\\s*)*[A-Z](?:[\\d_A-Z]*[a-z]\\w*)?\\b"), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\\s*[(){}[\\]<>=%~.:,;?+\\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[<>(),.:]")), + GrammarToken("operator", compileHighlightPattern("[?&|]")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g5), + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); + +final Grammar _g6 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g5), + GrammarToken("punctuation", compileHighlightPattern("\\.")), + GrammarToken("operator", compileHighlightPattern("\\*")), + GrammarToken("class-name", compileHighlightPattern("\\w+")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken( + "namespace", + compileHighlightPattern( + "^[a-z]\\w*(?:\\s*\\.\\s*[a-z]\\w*)*(?:\\s*\\.)?"), + inside: () => _g5), + GrammarToken("static", compileHighlightPattern("\\b\\w+\$")), + GrammarToken("punctuation", compileHighlightPattern("\\.")), + GrammarToken("operator", compileHighlightPattern("\\*")), + GrammarToken("class-name", compileHighlightPattern("\\w+")), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/scss.dart b/lib/highlight/scss.dart new file mode 100644 index 0000000..7470521 --- /dev/null +++ b/lib/highlight/scss.dart @@ -0,0 +1,94 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `scss`. +/// +/// Import this library only when you need `scss` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightScss { + /// The grammar for `scss`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])(?:\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*)"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "@(?:content|debug|each|else(?: if)?|extend|for|forward|function|if|import|include|mixin|return|use|warn|while)\\b", + caseSensitive: false)), + GrammarToken("keyword", compileHighlightPattern("( )(?:from|through)(?= )"), + lookbehind: true), + GrammarToken( + "atrule", + compileHighlightPattern( + "@[\\w-](?:\\([^()]+\\)|[^()\\s]|\\s+(?!\\s))*?(?=\\s+[{;])"), + inside: () => _g1), + GrammarToken("url", + compileHighlightPattern("(?:[-a-z]+-)?url(?=\\()", caseSensitive: false)), + GrammarToken( + "selector", + compileHighlightPattern( + "(?=\\S)[^@;{}()]?(?:[^@;{}()\\s]|\\s+(?!\\s)|#\\{\\\$[-\\w]+\\})+(?=\\s*\\{(?:\\}|\\s|[^}][^:{}]*[:{][^}]))"), + inside: () => _g2), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken( + "property", + compileHighlightPattern( + "(?:[-\\w]|\\\$[-\\w]|#\\{\\\$[-\\w]+\\})+(?=\\s*:)"), + inside: () => _g3), + GrammarToken( + "variable", compileHighlightPattern("\\\$[-\\w]+|#\\{\\\$[-\\w]+\\}")), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), + GrammarToken( + "module-modifier", + compileHighlightPattern("\\b(?:as|hide|show|with)\\b", + caseSensitive: false), + alias: "keyword"), + GrammarToken("placeholder", compileHighlightPattern("%[-\\w]+"), + alias: "selector"), + GrammarToken( + "statement", + compileHighlightPattern("\\B!(?:default|optional)\\b", + caseSensitive: false), + alias: "keyword"), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("null", compileHighlightPattern("\\bnull\\b"), alias: "keyword"), + GrammarToken( + "operator", + compileHighlightPattern( + "(\\s)(?:[-+*\\/%]|[=!]=|<=?|>=?|and|not|or)(?=\\s)"), + lookbehind: true), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("rule", compileHighlightPattern("@[\\w-]+")), +], rest: () => _g0); + +final Grammar _g2 = Grammar([ + GrammarToken("parent", compileHighlightPattern("&"), alias: "important"), + GrammarToken("placeholder", compileHighlightPattern("%[-\\w]+")), + GrammarToken( + "variable", compileHighlightPattern("\\\$[-\\w]+|#\\{\\\$[-\\w]+\\}")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "variable", compileHighlightPattern("\\\$[-\\w]+|#\\{\\\$[-\\w]+\\}")), +]); diff --git a/lib/highlight/solidity.dart b/lib/highlight/solidity.dart new file mode 100644 index 0000000..ed1c149 --- /dev/null +++ b/lib/highlight/solidity.dart @@ -0,0 +1,55 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `solidity`. +/// +/// Import this library only when you need `solidity` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightSolidity { + /// The grammar for `solidity`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:contract|enum|interface|library|new|struct|using)\\s+)(?!\\d)[\\w\$]+"), + lookbehind: true), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:address|bool|byte|u?int(?:8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)?|string|bytes(?:[1-9]|[12]\\d|3[0-2])?)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:_|anonymous|as|assembly|assert|break|calldata|case|constant|constructor|continue|contract|default|delete|do|else|emit|enum|event|external|for|from|function|if|import|indexed|inherited|interface|internal|is|let|library|mapping|memory|modifier|new|payable|pragma|private|public|pure|require|returns?|revert|selfdestruct|solidity|storage|struct|suicide|switch|this|throw|using|var|view|while)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "version", compileHighlightPattern("([<>]=?|\\^)\\d+\\.\\d+\\.\\d+\\b"), + lookbehind: true, alias: "number"), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\da-f]+\\b|(?:\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+)(?:e[+-]?\\d+)?", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "=>|->|:=|=:|\\*\\*|\\+\\+|--|\\|\\||&&|<<=?|>>=?|[-+*/%^&|<>!=]=?|[~?]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); diff --git a/lib/highlight/sql.dart b/lib/highlight/sql.dart new file mode 100644 index 0000000..eb4e7fb --- /dev/null +++ b/lib/highlight/sql.dart @@ -0,0 +1,64 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `sql`. +/// +/// Import this library only when you need `sql` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightSql { + /// The grammar for `sql`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "(^|[^\\\\])(?:\\/\\*[\\s\\S]*?\\*\\/|(?:--|\\/\\/|#).*)"), + lookbehind: true), + GrammarToken("variable", + compileHighlightPattern("@([\"'`])(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])+\\1"), + greedy: true), + GrammarToken("variable", compileHighlightPattern("@[\\w.\$]+")), + GrammarToken( + "string", + compileHighlightPattern( + "(^|[^@\\\\])(\"|')(?:\\\\[\\s\\S]|(?!\\2)[^\\\\]|\\2\\2)*\\2"), + lookbehind: true, + greedy: true), + GrammarToken("identifier", + compileHighlightPattern("(^|[^@\\\\])`(?:\\\\[\\s\\S]|[^`\\\\]|``)*`"), + lookbehind: true, greedy: true, inside: () => _g1), + GrammarToken( + "function", + compileHighlightPattern( + "\\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\\s*\\()", + caseSensitive: false)), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\\b", + caseSensitive: false)), + GrammarToken( + "boolean", + compileHighlightPattern("\\b(?:FALSE|NULL|TRUE)\\b", + caseSensitive: false)), + GrammarToken( + "number", + compileHighlightPattern( + "\\b0x[\\da-f]+\\b|\\b\\d+(?:\\.\\d*)?|\\B\\.\\d+\\b", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "[-+*\\/=%^~]|&&?|\\|\\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\\b", + caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("[;[\\]()`,.]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^`|`\$")), +]); diff --git a/lib/highlight/swift.dart b/lib/highlight/swift.dart new file mode 100644 index 0000000..f3faf42 --- /dev/null +++ b/lib/highlight/swift.dart @@ -0,0 +1,113 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `swift`. +/// +/// Import this library only when you need `swift` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightSwift { + /// The grammar for `swift`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", + compileHighlightPattern( + "(^|[^\\\\:])(?:\\/\\/.*|\\/\\*(?:[^/*]|\\/(?!\\*)|\\*(?!\\/)|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*\\*\\/)"), + lookbehind: true, + greedy: true), + GrammarToken( + "string-literal", + compileHighlightPattern( + "(^|[^\"#])(?:\"(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|\\r\\n|[^(])|[^\\\\\\r\\n\"])*\"|\"\"\"(?:\\\\(?:\\((?:[^()]|\\([^()]*\\))*\\)|[^(])|[^\\\\\"]|\"(?!\"\"))*\"\"\")(?![\"#])"), + lookbehind: true, + greedy: true, + inside: () => _g1), + GrammarToken( + "string-literal", + compileHighlightPattern( + "(^|[^\"#])(#+)(?:\"(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|\\r\\n|[^#])|[^\\\\\\r\\n])*?\"|\"\"\"(?:\\\\(?:#+\\((?:[^()]|\\([^()]*\\))*\\)|[^#])|[^\\\\])*?\"\"\")\\2"), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "directive", + compileHighlightPattern( + "#(?:(?:elseif|if)\\b(?:[ \t]*(?:![ \\t]*)?(?:\\b\\w+\\b(?:[ \\t]*\\((?:[^()]|\\([^()]*\\))*\\))?|\\((?:[^()]|\\([^()]*\\))*\\))(?:[ \\t]*(?:&&|\\|\\|))?)+|(?:else|endif)\\b)"), + alias: "property", + inside: () => _g3), + GrammarToken( + "literal", + compileHighlightPattern( + "#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\\b"), + alias: "constant"), + GrammarToken("other-directive", compileHighlightPattern("#\\w+\\b"), + alias: "property"), + GrammarToken("attribute", compileHighlightPattern("@\\w+"), alias: "atrule"), + GrammarToken( + "function-definition", compileHighlightPattern("(\\bfunc\\s+)\\w+"), + lookbehind: true, alias: "function"), + GrammarToken( + "label", + compileHighlightPattern( + "\\b(break|continue)\\s+\\w+|\\b[a-zA-Z_]\\w*(?=\\s*:\\s*(?:for|repeat|while)\\b)"), + lookbehind: true, + alias: "important"), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("nil", compileHighlightPattern("\\bnil\\b"), alias: "constant"), + GrammarToken("short-argument", compileHighlightPattern("\\\$\\d+\\b")), + GrammarToken("omit", compileHighlightPattern("\\b_\\b"), alias: "keyword"), + GrammarToken( + "number", + compileHighlightPattern( + "\\b(?:[\\d_]+(?:\\.[\\de_]+)?|0x[a-f0-9_]+(?:\\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b", + caseSensitive: false)), + GrammarToken("class-name", + compileHighlightPattern("\\b[A-Z](?:[A-Z_\\d]*[a-z]\\w*)?\\b")), + GrammarToken( + "function", + compileHighlightPattern("\\b[a-z_]\\w*(?=\\s*\\()", + caseSensitive: false)), + GrammarToken("constant", + compileHighlightPattern("\\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\\b")), + GrammarToken("operator", + compileHighlightPattern("[-+*/%=!<>&|^~?]+|\\.[.\\-+*/%=!<>&|^~?]+")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\]();,.:\\\\]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("interpolation", + compileHighlightPattern("(\\\\\\()(?:[^()]|\\([^()]*\\))*(?=\\))"), + lookbehind: true, inside: () => _g0), + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\)|\\\\\\(\$"), + alias: "punctuation"), + GrammarToken("punctuation", compileHighlightPattern("\\\\(?=[\\r\\n])")), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("interpolation", + compileHighlightPattern("(\\\\#+\\()(?:[^()]|\\([^()]*\\))*(?=\\))"), + lookbehind: true, inside: () => _g0), + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\)|\\\\#+\\(\$"), + alias: "punctuation"), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("directive-name", compileHighlightPattern("^#\\w+")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("number", compileHighlightPattern("\\b\\d+(?:\\.\\d+)*\\b")), + GrammarToken("operator", compileHighlightPattern("!|&&|\\|\\||[<>]=?")), + GrammarToken("punctuation", compileHighlightPattern("[(),]")), +]); diff --git a/lib/highlight/themes.dart b/lib/highlight/themes.dart new file mode 100644 index 0000000..4df6e94 --- /dev/null +++ b/lib/highlight/themes.dart @@ -0,0 +1,136 @@ +import 'package:flutter/painting.dart'; + +import '../highlight.dart'; + +/// Ready-made GitHub code themes for [MarkdownHighlighter]. +/// +/// Each theme is a separate `const` value; referencing one never retains the +/// other, so the theme you don't use is dropped from the build. +abstract final class HighlightThemes { + /// GitHub "dark" code theme (background `#0D1117`). + static const CodeHighlightTheme githubDark = _GithubTheme( + background: Color(0xFF0D1117), + foreground: Color(0xFFC9D1D9), + comment: Color(0xFF8B949E), + keyword: Color(0xFFFF7B72), + string: Color(0xFFA5D6FF), + number: Color(0xFF79C0FF), + function: Color(0xFFD2A8FF), + className: Color(0xFFFFA657), + variable: Color(0xFF79C0FF), + punctuation: Color(0xFFC9D1D9), + ); + + /// GitHub "light" code theme (background `#F6F8FA`). + static const CodeHighlightTheme githubLight = _GithubTheme( + background: Color(0xFFF6F8FA), + foreground: Color(0xFF24292F), + comment: Color(0xFF6E7781), + keyword: Color(0xFFCF222E), + string: Color(0xFF0A3069), + number: Color(0xFF0550AE), + function: Color(0xFF8250DF), + className: Color(0xFF953800), + variable: Color(0xFF0550AE), + punctuation: Color(0xFF24292F), + ); +} + +/// A GitHub-style theme parameterized by a small palette. Token classes are +/// resolved with a `switch` (no shared map), so each instance is self-contained +/// and tree-shakeable. +final class _GithubTheme implements CodeHighlightTheme { + const _GithubTheme({ + required Color background, + required Color foreground, + required this.comment, + required this.keyword, + required this.string, + required this.number, + required this.function, + required this.className, + required this.variable, + required this.punctuation, + }) : _background = background, + _foreground = foreground; + + final Color _background; + final Color _foreground; + + /// Color for comments (also italicized). + final Color comment; + + /// Color for keywords and operators. + final Color keyword; + + /// Color for strings and characters. + final Color string; + + /// Color for numbers, booleans and constants. + final Color number; + + /// Color for function names. + final Color function; + + /// Color for class names, builtins and namespaces. + final Color className; + + /// Color for variables, properties and parameters. + final Color variable; + + /// Color for punctuation. + final Color punctuation; + + @override + Color? get background => _background; + + @override + Color? get foreground => _foreground; + + @override + TextStyle? styleFor(String tokenType) => switch (tokenType) { + 'comment' || + 'prolog' || + 'doctype' || + 'cdata' => + TextStyle(color: comment, fontStyle: FontStyle.italic), + 'keyword' || + 'operator' || + 'rule' || + 'atrule' || + 'selector' => + TextStyle(color: keyword), + 'string' || + 'string-literal' || + 'char' || + 'attr-value' || + 'regex' => + TextStyle(color: string), + 'number' || + 'boolean' || + 'constant' || + 'null' || + 'symbol' || + 'unit' => + TextStyle(color: number), + 'function' || 'function-name' => TextStyle(color: function), + 'class-name' || + 'builtin' || + 'namespace' || + 'important' || + 'tag' => + TextStyle(color: className), + 'variable' || + 'property' || + 'parameter' || + 'attr-name' || + 'shebang' || + 'environment' || + 'file-descriptor' || + 'for-or-select' || + 'assign-left' => + TextStyle(color: variable), + 'punctuation' => TextStyle(color: punctuation), + _ => null, + }; +} diff --git a/lib/highlight/toml.dart b/lib/highlight/toml.dart new file mode 100644 index 0000000..ba95a54 --- /dev/null +++ b/lib/highlight/toml.dart @@ -0,0 +1,54 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `toml`. +/// +/// Import this library only when you need `toml` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightToml { + /// The grammar for `toml`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("#.*"), greedy: true), + GrammarToken( + "table", + compileHighlightPattern( + "(^[\\t ]*\\[\\s*(?:\\[\\s*)?)(?:[\\w-]+|'[^'\\n\\r]*'|\"(?:\\\\.|[^\\\\\"\\r\\n])*\")(?:\\s*\\.\\s*(?:[\\w-]+|'[^'\\n\\r]*'|\"(?:\\\\.|[^\\\\\"\\r\\n])*\"))*(?=\\s*\\])", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "class-name"), + GrammarToken( + "key", + compileHighlightPattern( + "(^[\\t ]*|[{,]\\s*)(?:[\\w-]+|'[^'\\n\\r]*'|\"(?:\\\\.|[^\\\\\"\\r\\n])*\")(?:\\s*\\.\\s*(?:[\\w-]+|'[^'\\n\\r]*'|\"(?:\\\\.|[^\\\\\"\\r\\n])*\"))*(?=\\s*=)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "\"\"\"(?:\\\\[\\s\\S]|[^\\\\])*?\"\"\"|'''[\\s\\S]*?'''|'[^'\\n\\r]*'|\"(?:\\\\.|[^\\\\\"\\r\\n])*\""), + greedy: true), + GrammarToken( + "date", + compileHighlightPattern( + "\\b\\d{4}-\\d{2}-\\d{2}(?:[T\\s]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})?)?\\b", + caseSensitive: false), + alias: "number"), + GrammarToken( + "date", compileHighlightPattern("\\b\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?\\b"), + alias: "number"), + GrammarToken( + "number", + compileHighlightPattern( + "(?:\\b0(?:x[\\da-zA-Z]+(?:_[\\da-zA-Z]+)*|o[0-7]+(?:_[0-7]+)*|b[10]+(?:_[10]+)*))\\b|[-+]?\\b\\d+(?:_\\d+)*(?:\\.\\d+(?:_\\d+)*)?(?:[eE][+-]?\\d+(?:_\\d+)*)?\\b|[-+]?\\b(?:inf|nan)\\b")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[.,=[\\]{}]")), +]); diff --git a/lib/highlight/tsx.dart b/lib/highlight/tsx.dart new file mode 100644 index 0000000..52b70e6 --- /dev/null +++ b/lib/highlight/tsx.dart @@ -0,0 +1,996 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `tsx`. +/// +/// Import this library only when you need `tsx` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightTsx { + /// The grammar for `tsx`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("prolog", compileHighlightPattern("<\\?[\\s\\S]+?\\?>"), + greedy: true), + GrammarToken( + "doctype", + compileHighlightPattern( + "\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:[^<\"'\\]]|\"[^\"]*\"|'[^']*'|<(?!!--)|)*\\]\\s*)?>", + caseSensitive: false), + greedy: true, + inside: () => _g1), + GrammarToken( + "style", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g2), + GrammarToken( + "script", + compileHighlightPattern( + "(]*>)(?:))*\\]\\]>|(?!)", + caseSensitive: false), + lookbehind: true, + greedy: true, + inside: () => _g7), + GrammarToken( + "cdata", + compileHighlightPattern("", + caseSensitive: false), + greedy: true), + GrammarToken( + "tag", + compileHighlightPattern( + "(^|[^\\w\$]|(?=<\\/))(?:<\\/?(?:[\\w.:-]+(?:(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)+(?:[\\w.:\$-]+(?:=(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*'|[^\\s{'\"/>=]+|(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})))?|(?:\\{(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)*\\.{3}(?:[^{}]|(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\}))*\\})))*(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)*\\/?)?>)"), + lookbehind: true, + greedy: true, + inside: () => _g19), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g28), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new|type)\\s+)(?!keyof\\b)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?:\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?"), + lookbehind: true, + greedy: true, + inside: () => _g31), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g40), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|declare|is|keyof|readonly|require)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:asserts|infer|interface|module|namespace|type)\\b(?=\\s*(?:[{_\$a-zA-Z\\xA0-\\uFFFF]|\$))")), + GrammarToken( + "keyword", compileHighlightPattern("\\btype\\b(?=\\s*(?:[\\{*]|\$))")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken("decorator", compileHighlightPattern("@[\$\\w\\xA0-\\uFFFF]+"), + inside: () => _g46), + GrammarToken( + "generic-function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\\s*\\()"), + greedy: true, + inside: () => _g47), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\\b")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("internal-subset", + compileHighlightPattern("(^[^\\[]*\\[)[\\s\\S]+(?=\\]>\$)"), + lookbehind: true, greedy: true, inside: () => _g0), + GrammarToken("string", compileHighlightPattern("\"[^\"]*\"|'[^']*'"), + greedy: true), + GrammarToken("punctuation", compileHighlightPattern("^\$|[[\\]]")), + GrammarToken( + "doctype-tag", compileHighlightPattern("^DOCTYPE", caseSensitive: false)), + GrammarToken("name", compileHighlightPattern("[^\\s<>'\"]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g3), + GrammarToken("language-css", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g4), +]); + +final Grammar _g3 = Grammar([ + GrammarToken( + "language-css", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => _g4), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\/\\*[\\s\\S]*?\\*\\/")), + GrammarToken( + "atrule", + compileHighlightPattern( + "@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*?(?:;|(?=\\s*\\{))"), + inside: () => _g5), + GrammarToken( + "url", + compileHighlightPattern( + "\\burl\\((?:(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')|(?:[^\\\\\\r\\n()\"']|\\\\[\\s\\S])*)\\)", + caseSensitive: false), + greedy: true, + inside: () => _g6), + GrammarToken( + "selector", + compileHighlightPattern( + "(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*'))*(?=\\s*\\{)"), + lookbehind: true), + GrammarToken( + "string", + compileHighlightPattern( + "(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')"), + greedy: true), + GrammarToken( + "property", + compileHighlightPattern( + "(^|[^-\\w\\xA0-\\uFFFF])(?!\\s)[-_a-z\\xA0-\\uFFFF](?:(?!\\s)[-\\w\\xA0-\\uFFFF])*(?=\\s*:)", + caseSensitive: false), + lookbehind: true), + GrammarToken("important", + compileHighlightPattern("!important\\b", caseSensitive: false)), + GrammarToken( + "function", + compileHighlightPattern("(^|[^-a-z0-9])[-a-z0-9]+(?=\\()", + caseSensitive: false), + lookbehind: true), + GrammarToken("punctuation", compileHighlightPattern("[(){};:,]")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("rule", compileHighlightPattern("^@[\\w-]+")), + GrammarToken( + "selector-function-argument", + compileHighlightPattern( + "(\\bselector\\s*\\(\\s*(?![\\s)]))(?:[^()\\s]|\\s+(?![\\s)])|\\((?:[^()]|\\([^()]*\\))*\\))+(?=\\s*\\))"), + lookbehind: true, + alias: "selector"), + GrammarToken("keyword", + compileHighlightPattern("(^|[^\\w-])(?:and|not|only|or)(?![\\w-])"), + lookbehind: true), +], rest: () => _g4); + +final Grammar _g6 = Grammar([ + GrammarToken( + "function", compileHighlightPattern("^url", caseSensitive: false)), + GrammarToken("punctuation", compileHighlightPattern("^\\(|\\)\$")), + GrammarToken( + "string", + compileHighlightPattern( + "^(?:\"(?:\\\\(?:\\r\\n|[\\s\\S])|[^\"\\\\\\r\\n])*\"|'(?:\\\\(?:\\r\\n|[\\s\\S])|[^'\\\\\\r\\n])*')\$"), + alias: "url"), +]); + +final Grammar _g7 = Grammar([ + GrammarToken( + "included-cdata", + compileHighlightPattern("", + caseSensitive: false), + inside: () => _g8), + GrammarToken("language-javascript", compileHighlightPattern("[\\s\\S]+"), + inside: () => _g9), +]); + +final Grammar _g8 = Grammar([ + GrammarToken( + "language-javascript", + compileHighlightPattern("(^\$)", + caseSensitive: false), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "cdata", + compileHighlightPattern("^\$", + caseSensitive: false)), +]); + +final Grammar _g9 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g10), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new)\\s+)[\\w.\\\\]+"), + lookbehind: true, + inside: () => _g12), + GrammarToken( + "class-name", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$A-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\.(?:constructor|prototype))"), + lookbehind: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g13), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "parameter", + compileHighlightPattern( + "(function(?:\\s+(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)?\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\))"), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "parameter", + compileHighlightPattern( + "(^|[^\$\\w\\xA0-\\uFFFF])(?!\\s)[_\$a-z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*=>)", + caseSensitive: false), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "parameter", + compileHighlightPattern( + "(\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*=>)"), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "parameter", + compileHighlightPattern( + "((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![\$\\w\\xA0-\\uFFFF]))(?:(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*)\\(\\s*|\\]\\s*\\(\\s*)(?!\\s)(?:[^()\\s]|\\s+(?![\\s)])|\\([^()]*\\))+(?=\\s*\\)\\s*\\{)"), + lookbehind: true, + inside: () => _g9), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "literal-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*:)", + multiLine: true), + lookbehind: true, + alias: "property"), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g11), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g11 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g9); + +final Grammar _g12 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("[.\\\\]")), +]); + +final Grammar _g13 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g14), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g14 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g15), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g17), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g18), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g15 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g16), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g16 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g17 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g18 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g19 = Grammar([ + GrammarToken("tag", compileHighlightPattern("^<\\/?[^\\s>\\/]*"), + inside: () => _g20), + GrammarToken( + "script", + compileHighlightPattern( + "=(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"), + alias: "language-javascript", + inside: () => _g21), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:style)\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g22), + GrammarToken( + "special-attr", + compileHighlightPattern( + "(^|[\"'\\s])(?:on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel))\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))", + caseSensitive: false), + lookbehind: true, + inside: () => _g24), + GrammarToken( + "attr-value", + compileHighlightPattern( + "=(?!\\{)(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"|'(?:\\\\[\\s\\S]|[^\\\\'])*'|[^\\s'\">]+)"), + inside: () => _g26), + GrammarToken("punctuation", compileHighlightPattern("\\/?>")), + GrammarToken( + "spread", + compileHighlightPattern( + "(?:\\{(?:\\s|\\/\\/.*(?!.)|\\/\\*(?:[^*]|\\*(?!\\/))\\*\\/)*\\.{3}(?:[^{}]|(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\}))*\\})"), + inside: () => _g0), + GrammarToken("attr-name", compileHighlightPattern("[^\\s>\\/]+"), + inside: () => _g27), + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), +]); + +final Grammar _g20 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<\\/?")), + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), + GrammarToken( + "class-name", compileHighlightPattern("^[A-Z]\\w*(?:\\.[A-Z]\\w*)*\$")), +]); + +final Grammar _g21 = Grammar([ + GrammarToken("script-punctuation", compileHighlightPattern("^=(?=\\{)"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g22 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g23), +]); + +final Grammar _g23 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "css", inside: () => _g4), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g24 = Grammar([ + GrammarToken("attr-name", compileHighlightPattern("^[^\\s=]+")), + GrammarToken("attr-value", compileHighlightPattern("=[\\s\\S]+"), + inside: () => _g25), +]); + +final Grammar _g25 = Grammar([ + GrammarToken("value", + compileHighlightPattern("(^=\\s*([\"']|(?![\"'])))\\S[\\s\\S]*(?=\\2\$)"), + lookbehind: true, alias: "javascript", inside: () => _g9), + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("\"|'")), +]); + +final Grammar _g26 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("^(\\s*)[\"']|[\"']\$"), + lookbehind: true), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g27 = Grammar([ + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); + +final Grammar _g28 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g29), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g29 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g30); + +final Grammar _g30 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g28), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new|type)\\s+)(?!keyof\\b)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?:\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?"), + lookbehind: true, + greedy: true, + inside: () => _g31), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g40), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|declare|is|keyof|readonly|require)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:asserts|infer|interface|module|namespace|type)\\b(?=\\s*(?:[{_\$a-zA-Z\\xA0-\\uFFFF]|\$))")), + GrammarToken( + "keyword", compileHighlightPattern("\\btype\\b(?=\\s*(?:[\\{*]|\$))")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("decorator", compileHighlightPattern("@[\$\\w\\xA0-\\uFFFF]+"), + inside: () => _g46), + GrammarToken( + "generic-function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\\s*\\()"), + greedy: true, + inside: () => _g47), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\\b")), +]); + +final Grammar _g31 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g32), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g34), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|declare|is|keyof|readonly|require)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:asserts|infer|interface|module|namespace|type)\\b(?=\\s*(?:[{_\$a-zA-Z\\xA0-\\uFFFF]|\$))")), + GrammarToken( + "keyword", compileHighlightPattern("\\btype\\b(?=\\s*(?:[\\{*]|\$))")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\\b")), +]); + +final Grammar _g32 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g33), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g33 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g31); + +final Grammar _g34 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g35), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g35 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g36), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g38), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g39), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g36 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g37), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g37 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g38 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g39 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g40 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g41), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g41 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g42), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g44), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g45), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g42 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g43), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g43 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g44 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g45 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g46 = Grammar([ + GrammarToken("at", compileHighlightPattern("^@"), alias: "operator"), + GrammarToken("function", compileHighlightPattern("^[\\s\\S]+")), +]); + +final Grammar _g47 = Grammar([ + GrammarToken( + "function", + compileHighlightPattern( + "^#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*")), + GrammarToken("generic", compileHighlightPattern("<[\\s\\S]+"), + alias: "class-name", inside: () => _g31), +]); diff --git a/lib/highlight/typescript.dart b/lib/highlight/typescript.dart new file mode 100644 index 0000000..e50f2b0 --- /dev/null +++ b/lib/highlight/typescript.dart @@ -0,0 +1,417 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `typescript`. +/// +/// Import this library only when you need `typescript` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightTypescript { + /// The grammar for `typescript`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g1), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "class-name", + compileHighlightPattern( + "(\\b(?:class|extends|implements|instanceof|interface|new|type)\\s+)(?!keyof\\b)(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?:\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?"), + lookbehind: true, + greedy: true, + inside: () => _g3), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g12), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|declare|is|keyof|readonly|require)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:asserts|infer|interface|module|namespace|type)\\b(?=\\s*(?:[{_\$a-zA-Z\\xA0-\\uFFFF]|\$))")), + GrammarToken( + "keyword", compileHighlightPattern("\\btype\\b(?=\\s*(?:[\\{*]|\$))")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken("decorator", compileHighlightPattern("@[\$\\w\\xA0-\\uFFFF]+"), + inside: () => _g18), + GrammarToken( + "generic-function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\\s*\\()"), + greedy: true, + inside: () => _g19), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\\b")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g2), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g0); + +final Grammar _g3 = Grammar([ + GrammarToken("comment", + compileHighlightPattern("(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|\$)"), + lookbehind: true, greedy: true), + GrammarToken("comment", compileHighlightPattern("(^|[^\\\\:])\\/\\/.*"), + lookbehind: true, greedy: true), + GrammarToken("hashbang", compileHighlightPattern("^#!.*"), + greedy: true, alias: "comment"), + GrammarToken( + "template-string", + compileHighlightPattern( + "`(?:\\\\[\\s\\S]|\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}|(?!\\\$\\{)[^\\\\`])*`"), + greedy: true, + inside: () => _g4), + GrammarToken( + "string-property", + compileHighlightPattern( + "((?:^|[,{])[ \\t]*)([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\2)[^\\\\\\r\\n])*\\2(?=\\s*:)", + multiLine: true), + lookbehind: true, + greedy: true, + alias: "property"), + GrammarToken( + "string", + compileHighlightPattern( + "([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1"), + greedy: true), + GrammarToken( + "regex", + compileHighlightPattern( + "((?:^|[^\$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)\\/(?:(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.|\\[(?:[^[\\]\\\\\\r\\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|\\/\\*(?:[^*]|\\*(?!\\/))*\\*\\/)*(?:\$|[\\r\\n,.;:})\\]]|\\/\\/))"), + lookbehind: true, + greedy: true, + inside: () => _g6), + GrammarToken( + "function-variable", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*)\\s*=>))"), + alias: "function"), + GrammarToken( + "constant", compileHighlightPattern("\\b[A-Z](?:[A-Z_]|\\dx?)*\\b")), + GrammarToken("keyword", compileHighlightPattern("((?:^|\\})\\s*)catch\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|assert(?=\\s*\\{)|async(?=\\s*(?:function\\b|\\(|[\$\\w\\xA0-\\uFFFF]|\$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\\s*(?:\\{|\$))|for|from(?=\\s*(?:['\"]|\$))|function|(?:get|set)(?=\\s*(?:[#\\[\$\\w\\xA0-\\uFFFF]|\$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b"), + lookbehind: true), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:abstract|declare|is|keyof|readonly|require)\\b")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:asserts|infer|interface|module|namespace|type)\\b(?=\\s*(?:[{_\$a-zA-Z\\xA0-\\uFFFF]|\$))")), + GrammarToken( + "keyword", compileHighlightPattern("\\btype\\b(?=\\s*(?:[\\{*]|\$))")), + GrammarToken("boolean", compileHighlightPattern("\\b(?:false|true)\\b")), + GrammarToken( + "function", + compileHighlightPattern( + "#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()")), + GrammarToken( + "number", + compileHighlightPattern( + "(^|[^\\w\$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w\$])"), + lookbehind: true), + GrammarToken( + "operator", + compileHighlightPattern( + "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\];(),.:]")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\\b")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("template-punctuation", compileHighlightPattern("^`|`\$"), + alias: "string"), + GrammarToken( + "interpolation", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\{2})*)\\\$\\{(?:[^{}]|\\{(?:[^{}]|\\{[^}]*\\})*\\})+\\}"), + lookbehind: true, + inside: () => _g5), + GrammarToken("string", compileHighlightPattern("[\\s\\S]+")), +]); + +final Grammar _g5 = Grammar([ + GrammarToken( + "interpolation-punctuation", compileHighlightPattern("^\\\$\\{|\\}\$"), + alias: "punctuation"), +], rest: () => _g3); + +final Grammar _g6 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g7), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g7 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g8), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g10), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g11), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g8 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g9), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g9 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g10 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g11 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g12 = Grammar([ + GrammarToken( + "regex-source", compileHighlightPattern("^(\\/)[\\s\\S]+(?=\\/[a-z]*\$)"), + lookbehind: true, alias: "language-regex", inside: () => _g13), + GrammarToken("regex-delimiter", compileHighlightPattern("^\\/|\\/\$")), + GrammarToken("regex-flags", compileHighlightPattern("^[a-z]+\$")), +]); + +final Grammar _g13 = Grammar([ + GrammarToken( + "char-class", + compileHighlightPattern( + "((?:^|[^\\\\])(?:\\\\\\\\)*)\\[(?:[^\\\\\\]]|\\\\[\\s\\S])*\\]"), + lookbehind: true, + inside: () => _g14), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\.|\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "backreference", compileHighlightPattern("\\\\(?![123][0-7]{2})[1-9]"), + alias: "keyword"), + GrammarToken("backreference", compileHighlightPattern("\\\\k<[^<>']+>"), + alias: "keyword", inside: () => _g16), + GrammarToken("anchor", compileHighlightPattern("[\$^]|\\\\[ABbGZz]"), + alias: "function"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken( + "group", + compileHighlightPattern( + "\\((?:\\?(?:<[^<>']+>|'[^<>']+'|[>:]| _g17), + GrammarToken("group", compileHighlightPattern("\\)"), alias: "punctuation"), + GrammarToken("quantifier", + compileHighlightPattern("(?:[+*?]|\\{\\d+(?:,\\d*)?\\})[?+]?"), + alias: "number"), + GrammarToken("alternation", compileHighlightPattern("\\|"), alias: "keyword"), +]); + +final Grammar _g14 = Grammar([ + GrammarToken("char-class-negation", compileHighlightPattern("(^\\[)\\^"), + lookbehind: true, alias: "operator"), + GrammarToken("char-class-punctuation", compileHighlightPattern("^\\[|\\]\$"), + alias: "punctuation"), + GrammarToken( + "range", + compileHighlightPattern( + "(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))-(?:[^\\\\-]|\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.))"), + inside: () => _g15), + GrammarToken( + "special-escape", compileHighlightPattern("\\\\[\\\\(){}[\\]^\$+*?|.]"), + alias: "escape"), + GrammarToken( + "char-set", + compileHighlightPattern("\\\\[wsd]|\\\\p\\{[^{}]+\\}", + caseSensitive: false), + alias: "class-name"), + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), +]); + +final Grammar _g15 = Grammar([ + GrammarToken( + "escape", + compileHighlightPattern( + "\\\\(?:x[\\da-fA-F]{2}|u[\\da-fA-F]{4}|u\\{[\\da-fA-F]+\\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)")), + GrammarToken("range-punctuation", compileHighlightPattern("-"), + alias: "operator"), +]); + +final Grammar _g16 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g17 = Grammar([ + GrammarToken("group-name", compileHighlightPattern("(<|')[^<>']+(?=[>']\$)"), + lookbehind: true, alias: "variable"), +]); + +final Grammar _g18 = Grammar([ + GrammarToken("at", compileHighlightPattern("^@"), alias: "operator"), + GrammarToken("function", compileHighlightPattern("^[\\s\\S]+")), +]); + +final Grammar _g19 = Grammar([ + GrammarToken( + "function", + compileHighlightPattern( + "^#?(?!\\s)[_\$a-zA-Z\\xA0-\\uFFFF](?:(?!\\s)[\$\\w\\xA0-\\uFFFF])*")), + GrammarToken("generic", compileHighlightPattern("<[\\s\\S]+"), + alias: "class-name", inside: () => _g3), +]); diff --git a/lib/highlight/vim.dart b/lib/highlight/vim.dart new file mode 100644 index 0000000..64fc3f1 --- /dev/null +++ b/lib/highlight/vim.dart @@ -0,0 +1,40 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `vim`. +/// +/// Import this library only when you need `vim` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightVim { + /// The grammar for `vim`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "string", + compileHighlightPattern( + "\"(?:[^\"\\\\\\r\\n]|\\\\.)*\"|'(?:[^'\\r\\n]|'')*'")), + GrammarToken("comment", compileHighlightPattern("\".*")), + GrammarToken("function", compileHighlightPattern("\\b\\w+(?=\\()")), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:N|Next|P|Print|X|XMLent|XMLns|ab|abbreviate|abc|abclear|abo|aboveleft|al|all|ar|arga|argadd|argd|argdelete|argdo|arge|argedit|argg|argglobal|argl|arglocal|args|argu|argument|as|ascii|b|bN|bNext|ba|bad|badd|ball|bd|bdelete|be|bel|belowright|bf|bfirst|bl|blast|bm|bmodified|bn|bnext|bo|botright|bp|bprevious|br|brea|break|breaka|breakadd|breakd|breakdel|breakl|breaklist|brewind|bro|browse|bufdo|buffer|buffers|bun|bunload|bw|bwipeout|c|cN|cNext|cNfcNfile|ca|cabbrev|cabc|cabclear|cad|caddb|caddbuffer|caddexpr|caddf|caddfile|cal|call|cat|catch|cb|cbuffer|cc|ccl|cclose|cd|ce|center|cex|cexpr|cf|cfile|cfir|cfirst|cg|cgetb|cgetbuffer|cgete|cgetexpr|cgetfile|change|changes|chd|chdir|che|checkpath|checkt|checktime|cl|cla|clast|clist|clo|close|cmapc|cmapclear|cn|cnew|cnewer|cnext|cnf|cnfile|cnorea|cnoreabbrev|co|col|colder|colo|colorscheme|comc|comclear|comp|compiler|con|conf|confirm|continue|cope|copen|copy|cp|cpf|cpfile|cprevious|cq|cquit|cr|crewind|cu|cuna|cunabbrev|cunmap|cw|cwindow|d|debugg|debuggreedy|delc|delcommand|delete|delf|delfunction|delm|delmarks|di|diffg|diffget|diffoff|diffpatch|diffpu|diffput|diffsplit|diffthis|diffu|diffupdate|dig|digraphs|display|dj|djump|dl|dlist|dr|drop|ds|dsearch|dsp|dsplit|e|earlier|echoe|echoerr|echom|echomsg|echon|edit|el|else|elsei|elseif|em|emenu|en|endf|endfo|endfor|endfun|endfunction|endif|endt|endtry|endw|endwhile|ene|enew|ex|exi|exit|exu|exusage|f|file|files|filetype|fin|fina|finally|find|fini|finish|fir|first|fix|fixdel|fo|fold|foldc|foldclose|foldd|folddoc|folddoclosed|folddoopen|foldo|foldopen|for|fu|fun|function|go|goto|gr|grep|grepa|grepadd|h|ha|hardcopy|help|helpf|helpfind|helpg|helpgrep|helpt|helptags|hid|hide|his|history|ia|iabbrev|iabc|iabclear|if|ij|ijump|il|ilist|imapc|imapclear|in|inorea|inoreabbrev|isearch|isp|isplit|iu|iuna|iunabbrev|iunmap|j|join|ju|jumps|k|kee|keepalt|keepj|keepjumps|keepmarks|l|lN|lNext|lNf|lNfile|la|lad|laddb|laddbuffer|laddexpr|laddf|laddfile|lan|language|last|later|lb|lbuffer|lc|lcd|lch|lchdir|lcl|lclose|left|lefta|leftabove|let|lex|lexpr|lf|lfile|lfir|lfirst|lg|lgetb|lgetbuffer|lgete|lgetexpr|lgetfile|lgr|lgrep|lgrepa|lgrepadd|lh|lhelpgrep|list|ll|lla|llast|lli|llist|lm|lmak|lmake|lmap|lmapc|lmapclear|ln|lne|lnew|lnewer|lnext|lnf|lnfile|lnoremap|lo|loadview|loc|lockmarks|lockv|lockvar|lol|lolder|lop|lopen|lp|lpf|lpfile|lprevious|lr|lrewind|ls|lt|ltag|lu|lunmap|lv|lvimgrep|lvimgrepa|lvimgrepadd|lw|lwindow|m|ma|mak|make|mark|marks|mat|match|menut|menutranslate|mk|mkexrc|mks|mksession|mksp|mkspell|mkv|mkvie|mkview|mkvimrc|mod|mode|move|mz|mzf|mzfile|mzscheme|n|nbkey|new|next|nmapc|nmapclear|noh|nohlsearch|norea|noreabbrev|nu|number|nun|nunmap|o|omapc|omapclear|on|only|open|opt|options|ou|ounmap|p|pc|pclose|pe|ped|pedit|perl|perld|perldo|po|pop|popu|popup|pp|ppop|pre|preserve|prev|previous|print|prof|profd|profdel|profile|promptf|promptfind|promptr|promptrepl|ps|psearch|ptN|ptNext|pta|ptag|ptf|ptfirst|ptj|ptjump|ptl|ptlast|ptn|ptnext|ptp|ptprevious|ptr|ptrewind|pts|ptselect|pu|put|pw|pwd|py|pyf|pyfile|python|q|qa|qall|quit|quita|quitall|r|read|rec|recover|red|redi|redir|redo|redr|redraw|redraws|redrawstatus|reg|registers|res|resize|ret|retab|retu|return|rew|rewind|ri|right|rightb|rightbelow|ru|rub|ruby|rubyd|rubydo|rubyf|rubyfile|runtime|rv|rviminfo|sN|sNext|sa|sal|sall|san|sandbox|sargument|sav|saveas|sb|sbN|sbNext|sba|sball|sbf|sbfirst|sbl|sblast|sbm|sbmodified|sbn|sbnext|sbp|sbprevious|sbr|sbrewind|sbuffer|scrip|scripte|scriptencoding|scriptnames|se|set|setf|setfiletype|setg|setglobal|setl|setlocal|sf|sfind|sfir|sfirst|sh|shell|sign|sil|silent|sim|simalt|sl|sla|slast|sleep|sm|smagic|smap|smapc|smapclear|sme|smenu|sn|snext|sni|sniff|sno|snomagic|snor|snoremap|snoreme|snoremenu|so|sor|sort|source|sp|spe|spelld|spelldump|spellgood|spelli|spellinfo|spellr|spellrepall|spellu|spellundo|spellw|spellwrong|split|spr|sprevious|sre|srewind|st|sta|stag|star|startg|startgreplace|startinsert|startr|startreplace|stj|stjump|stop|stopi|stopinsert|sts|stselect|sun|sunhide|sunm|sunmap|sus|suspend|sv|sview|syncbind|t|tN|tNext|ta|tab|tabN|tabNext|tabc|tabclose|tabd|tabdo|tabe|tabedit|tabf|tabfind|tabfir|tabfirst|tabl|tablast|tabm|tabmove|tabn|tabnew|tabnext|tabo|tabonly|tabp|tabprevious|tabr|tabrewind|tabs|tag|tags|tc|tcl|tcld|tcldo|tclf|tclfile|te|tearoff|tf|tfirst|th|throw|tj|tjump|tl|tlast|tm|tmenu|tn|tnext|to|topleft|tp|tprevious|tr|trewind|try|ts|tselect|tu|tunmenu|u|una|unabbreviate|undo|undoj|undojoin|undol|undolist|unh|unhide|unlet|unlo|unlockvar|unm|unmap|up|update|ve|verb|verbose|version|vert|vertical|vi|vie|view|vim|vimgrep|vimgrepa|vimgrepadd|visual|viu|viusage|vmapc|vmapclear|vne|vnew|vs|vsplit|vu|vunmap|w|wN|wNext|wa|wall|wh|while|win|winc|wincmd|windo|winp|winpos|winsize|wn|wnext|wp|wprevious|wq|wqa|wqall|write|ws|wsverb|wv|wviminfo|x|xa|xall|xit|xm|xmap|xmapc|xmapclear|xme|xmenu|xn|xnoremap|xnoreme|xnoremenu|xu|xunmap|y|yank)\\b")), + GrammarToken( + "builtin", + compileHighlightPattern( + "\\b(?:acd|ai|akm|aleph|allowrevins|altkeymap|ambiwidth|ambw|anti|antialias|arab|arabic|arabicshape|ari|arshape|autochdir|autocmd|autoindent|autoread|autowrite|autowriteall|aw|awa|background|backspace|backup|backupcopy|backupdir|backupext|backupskip|balloondelay|ballooneval|balloonexpr|bdir|bdlay|beval|bex|bexpr|bg|bh|bin|binary|biosk|bioskey|bk|bkc|bomb|breakat|brk|browsedir|bs|bsdir|bsk|bt|bufhidden|buflisted|buftype|casemap|ccv|cdpath|cedit|cfu|ch|charconvert|ci|cin|cindent|cink|cinkeys|cino|cinoptions|cinw|cinwords|clipboard|cmdheight|cmdwinheight|cmp|cms|columns|com|comments|commentstring|compatible|complete|completefunc|completeopt|consk|conskey|copyindent|cot|cpo|cpoptions|cpt|cscopepathcomp|cscopeprg|cscopequickfix|cscopetag|cscopetagorder|cscopeverbose|cspc|csprg|csqf|cst|csto|csverb|cuc|cul|cursorcolumn|cursorline|cwh|debug|deco|def|define|delcombine|dex|dg|dict|dictionary|diff|diffexpr|diffopt|digraph|dip|dir|directory|dy|ea|ead|eadirection|eb|ed|edcompatible|ef|efm|ei|ek|enc|encoding|endofline|eol|ep|equalalways|equalprg|errorbells|errorfile|errorformat|esckeys|et|eventignore|expandtab|exrc|fcl|fcs|fdc|fde|fdi|fdl|fdls|fdm|fdn|fdo|fdt|fen|fenc|fencs|fex|ff|ffs|fileencoding|fileencodings|fileformat|fileformats|fillchars|fk|fkmap|flp|fml|fmr|foldcolumn|foldenable|foldexpr|foldignore|foldlevel|foldlevelstart|foldmarker|foldmethod|foldminlines|foldnestmax|foldtext|formatexpr|formatlistpat|formatoptions|formatprg|fp|fs|fsync|ft|gcr|gd|gdefault|gfm|gfn|gfs|gfw|ghr|gp|grepformat|grepprg|gtl|gtt|guicursor|guifont|guifontset|guifontwide|guiheadroom|guioptions|guipty|guitablabel|guitabtooltip|helpfile|helpheight|helplang|hf|hh|hi|hidden|highlight|hk|hkmap|hkmapp|hkp|hl|hlg|hls|hlsearch|ic|icon|iconstring|ignorecase|im|imactivatekey|imak|imc|imcmdline|imd|imdisable|imi|iminsert|ims|imsearch|inc|include|includeexpr|incsearch|inde|indentexpr|indentkeys|indk|inex|inf|infercase|insertmode|invacd|invai|invakm|invallowrevins|invaltkeymap|invanti|invantialias|invar|invarab|invarabic|invarabicshape|invari|invarshape|invautochdir|invautoindent|invautoread|invautowrite|invautowriteall|invaw|invawa|invbackup|invballooneval|invbeval|invbin|invbinary|invbiosk|invbioskey|invbk|invbl|invbomb|invbuflisted|invcf|invci|invcin|invcindent|invcompatible|invconfirm|invconsk|invconskey|invcopyindent|invcp|invcscopetag|invcscopeverbose|invcst|invcsverb|invcuc|invcul|invcursorcolumn|invcursorline|invdeco|invdelcombine|invdg|invdiff|invdigraph|invdisable|invea|inveb|inved|invedcompatible|invek|invendofline|inveol|invequalalways|inverrorbells|invesckeys|invet|invex|invexpandtab|invexrc|invfen|invfk|invfkmap|invfoldenable|invgd|invgdefault|invguipty|invhid|invhidden|invhk|invhkmap|invhkmapp|invhkp|invhls|invhlsearch|invic|invicon|invignorecase|invim|invimc|invimcmdline|invimd|invincsearch|invinf|invinfercase|invinsertmode|invis|invjoinspaces|invjs|invlazyredraw|invlbr|invlinebreak|invlisp|invlist|invloadplugins|invlpl|invlz|invma|invmacatsui|invmagic|invmh|invml|invmod|invmodeline|invmodifiable|invmodified|invmore|invmousef|invmousefocus|invmousehide|invnu|invnumber|invodev|invopendevice|invpaste|invpi|invpreserveindent|invpreviewwindow|invprompt|invpvw|invreadonly|invremap|invrestorescreen|invrevins|invri|invrightleft|invrightleftcmd|invrl|invrlc|invro|invrs|invru|invruler|invsb|invsc|invscb|invscrollbind|invscs|invsecure|invsft|invshellslash|invshelltemp|invshiftround|invshortname|invshowcmd|invshowfulltag|invshowmatch|invshowmode|invsi|invsm|invsmartcase|invsmartindent|invsmarttab|invsmd|invsn|invsol|invspell|invsplitbelow|invsplitright|invspr|invsr|invssl|invsta|invstartofline|invstmp|invswapfile|invswf|invta|invtagbsearch|invtagrelative|invtagstack|invtbi|invtbidi|invtbs|invtermbidi|invterse|invtextauto|invtextmode|invtf|invtgst|invtildeop|invtimeout|invtitle|invto|invtop|invtr|invttimeout|invttybuiltin|invttyfast|invtx|invvb|invvisualbell|invwa|invwarn|invwb|invweirdinvert|invwfh|invwfw|invwildmenu|invwinfixheight|invwinfixwidth|invwiv|invwmnu|invwrap|invwrapscan|invwrite|invwriteany|invwritebackup|invws|isf|isfname|isi|isident|isk|iskeyword|isprint|joinspaces|js|key|keymap|keymodel|keywordprg|km|kmp|kp|langmap|langmenu|laststatus|lazyredraw|lbr|lcs|linebreak|lines|linespace|lisp|lispwords|listchars|loadplugins|lpl|lsp|lz|macatsui|magic|makeef|makeprg|matchpairs|matchtime|maxcombine|maxfuncdepth|maxmapdepth|maxmem|maxmempattern|maxmemtot|mco|mef|menuitems|mfd|mh|mis|mkspellmem|ml|mls|mm|mmd|mmp|mmt|modeline|modelines|modifiable|modified|more|mouse|mousef|mousefocus|mousehide|mousem|mousemodel|mouses|mouseshape|mouset|mousetime|mp|mps|msm|mzq|mzquantum|nf|noacd|noai|noakm|noallowrevins|noaltkeymap|noanti|noantialias|noar|noarab|noarabic|noarabicshape|noari|noarshape|noautochdir|noautoindent|noautoread|noautowrite|noautowriteall|noaw|noawa|nobackup|noballooneval|nobeval|nobin|nobinary|nobiosk|nobioskey|nobk|nobl|nobomb|nobuflisted|nocf|noci|nocin|nocindent|nocompatible|noconfirm|noconsk|noconskey|nocopyindent|nocp|nocscopetag|nocscopeverbose|nocst|nocsverb|nocuc|nocul|nocursorcolumn|nocursorline|nodeco|nodelcombine|nodg|nodiff|nodigraph|nodisable|noea|noeb|noed|noedcompatible|noek|noendofline|noeol|noequalalways|noerrorbells|noesckeys|noet|noex|noexpandtab|noexrc|nofen|nofk|nofkmap|nofoldenable|nogd|nogdefault|noguipty|nohid|nohidden|nohk|nohkmap|nohkmapp|nohkp|nohls|noic|noicon|noignorecase|noim|noimc|noimcmdline|noimd|noincsearch|noinf|noinfercase|noinsertmode|nois|nojoinspaces|nojs|nolazyredraw|nolbr|nolinebreak|nolisp|nolist|noloadplugins|nolpl|nolz|noma|nomacatsui|nomagic|nomh|noml|nomod|nomodeline|nomodifiable|nomodified|nomore|nomousef|nomousefocus|nomousehide|nonu|nonumber|noodev|noopendevice|nopaste|nopi|nopreserveindent|nopreviewwindow|noprompt|nopvw|noreadonly|noremap|norestorescreen|norevins|nori|norightleft|norightleftcmd|norl|norlc|noro|nors|noru|noruler|nosb|nosc|noscb|noscrollbind|noscs|nosecure|nosft|noshellslash|noshelltemp|noshiftround|noshortname|noshowcmd|noshowfulltag|noshowmatch|noshowmode|nosi|nosm|nosmartcase|nosmartindent|nosmarttab|nosmd|nosn|nosol|nospell|nosplitbelow|nosplitright|nospr|nosr|nossl|nosta|nostartofline|nostmp|noswapfile|noswf|nota|notagbsearch|notagrelative|notagstack|notbi|notbidi|notbs|notermbidi|noterse|notextauto|notextmode|notf|notgst|notildeop|notimeout|notitle|noto|notop|notr|nottimeout|nottybuiltin|nottyfast|notx|novb|novisualbell|nowa|nowarn|nowb|noweirdinvert|nowfh|nowfw|nowildmenu|nowinfixheight|nowinfixwidth|nowiv|nowmnu|nowrap|nowrapscan|nowrite|nowriteany|nowritebackup|nows|nrformats|numberwidth|nuw|odev|oft|ofu|omnifunc|opendevice|operatorfunc|opfunc|osfiletype|pa|para|paragraphs|paste|pastetoggle|patchexpr|patchmode|path|pdev|penc|pex|pexpr|pfn|ph|pheader|pi|pm|pmbcs|pmbfn|popt|preserveindent|previewheight|previewwindow|printdevice|printencoding|printexpr|printfont|printheader|printmbcharset|printmbfont|printoptions|prompt|pt|pumheight|pvh|pvw|qe|quoteescape|readonly|remap|report|restorescreen|revins|rightleft|rightleftcmd|rl|rlc|ro|rs|rtp|ruf|ruler|rulerformat|runtimepath|sbo|sc|scb|scr|scroll|scrollbind|scrolljump|scrolloff|scrollopt|scs|sect|sections|secure|sel|selection|selectmode|sessionoptions|sft|shcf|shellcmdflag|shellpipe|shellquote|shellredir|shellslash|shelltemp|shelltype|shellxquote|shiftround|shiftwidth|shm|shortmess|shortname|showbreak|showcmd|showfulltag|showmatch|showmode|showtabline|shq|si|sidescroll|sidescrolloff|siso|sj|slm|smartcase|smartindent|smarttab|smc|smd|softtabstop|sol|spc|spell|spellcapcheck|spellfile|spelllang|spellsuggest|spf|spl|splitbelow|splitright|sps|sr|srr|ss|ssl|ssop|stal|startofline|statusline|stl|stmp|su|sua|suffixes|suffixesadd|sw|swapfile|swapsync|swb|swf|switchbuf|sws|sxq|syn|synmaxcol|syntax|t_AB|t_AF|t_AL|t_CS|t_CV|t_Ce|t_Co|t_Cs|t_DL|t_EI|t_F1|t_F2|t_F3|t_F4|t_F5|t_F6|t_F7|t_F8|t_F9|t_IE|t_IS|t_K1|t_K3|t_K4|t_K5|t_K6|t_K7|t_K8|t_K9|t_KA|t_KB|t_KC|t_KD|t_KE|t_KF|t_KG|t_KH|t_KI|t_KJ|t_KK|t_KL|t_RI|t_RV|t_SI|t_Sb|t_Sf|t_WP|t_WS|t_ZH|t_ZR|t_al|t_bc|t_cd|t_ce|t_cl|t_cm|t_cs|t_da|t_db|t_dl|t_fs|t_k1|t_k2|t_k3|t_k4|t_k5|t_k6|t_k7|t_k8|t_k9|t_kB|t_kD|t_kI|t_kN|t_kP|t_kb|t_kd|t_ke|t_kh|t_kl|t_kr|t_ks|t_ku|t_le|t_mb|t_md|t_me|t_mr|t_ms|t_nd|t_op|t_se|t_so|t_sr|t_te|t_ti|t_ts|t_ue|t_us|t_ut|t_vb|t_ve|t_vi|t_vs|t_xs|tabline|tabpagemax|tabstop|tagbsearch|taglength|tagrelative|tagstack|tal|tb|tbi|tbidi|tbis|tbs|tenc|term|termbidi|termencoding|terse|textauto|textmode|textwidth|tgst|thesaurus|tildeop|timeout|timeoutlen|title|titlelen|titleold|titlestring|toolbar|toolbariconsize|top|tpm|tsl|tsr|ttimeout|ttimeoutlen|ttm|tty|ttybuiltin|ttyfast|ttym|ttymouse|ttyscroll|ttytype|tw|tx|uc|ul|undolevels|updatecount|updatetime|ut|vb|vbs|vdir|verbosefile|vfile|viewdir|viewoptions|viminfo|virtualedit|visualbell|vop|wak|warn|wb|wc|wcm|wd|weirdinvert|wfh|wfw|whichwrap|wi|wig|wildchar|wildcharm|wildignore|wildmenu|wildmode|wildoptions|wim|winaltkeys|window|winfixheight|winfixwidth|winheight|winminheight|winminwidth|winwidth|wiv|wiw|wm|wmh|wmnu|wmw|wop|wrap|wrapmargin|wrapscan|writeany|writebackup|writedelay|ww)\\b")), + GrammarToken( + "number", + compileHighlightPattern("\\b(?:0x[\\da-f]+|\\d+(?:\\.\\d+)?)\\b", + caseSensitive: false)), + GrammarToken( + "operator", + compileHighlightPattern( + "\\|\\||&&|[-+.]=?|[=!](?:[=~][#?]?)?|[<>]=?[#?]?|[*\\/%?]|\\b(?:is(?:not)?)\\b")), + GrammarToken("punctuation", compileHighlightPattern("[{}[\\](),;:]")), +]); diff --git a/lib/highlight/wasm.dart b/lib/highlight/wasm.dart new file mode 100644 index 0000000..405d168 --- /dev/null +++ b/lib/highlight/wasm.dart @@ -0,0 +1,48 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `wasm`. +/// +/// Import this library only when you need `wasm` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightWasm { + /// The grammar for `wasm`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken("comment", compileHighlightPattern("\\(;[\\s\\S]*?;\\)")), + GrammarToken("comment", compileHighlightPattern(";;.*"), greedy: true), + GrammarToken( + "string", compileHighlightPattern("\"(?:\\\\[\\s\\S]|[^\"\\\\])*\""), + greedy: true), + GrammarToken("keyword", compileHighlightPattern("\\b(?:align|offset)="), + inside: () => _g1), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:(?:f32|f64|i32|i64)(?:\\.(?:abs|add|and|ceil|clz|const|convert_[su]\\/i(?:32|64)|copysign|ctz|demote\\/f64|div(?:_[su])?|eqz?|extend_[su]\\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|neg?|nearest|or|popcnt|promote\\/f32|reinterpret\\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|sqrt|store(?:8|16|32)?|sub|trunc(?:_[su]\\/f(?:32|64))?|wrap\\/i64|xor))?|memory\\.(?:grow|size))\\b"), + inside: () => _g2), + GrammarToken( + "keyword", + compileHighlightPattern( + "\\b(?:anyfunc|block|br(?:_if|_table)?|call(?:_indirect)?|data|drop|elem|else|end|export|func|get_(?:global|local)|global|if|import|local|loop|memory|module|mut|nop|offset|param|result|return|select|set_(?:global|local)|start|table|tee_local|then|type|unreachable)\\b")), + GrammarToken("variable", + compileHighlightPattern("\\\$[\\w!#\$%&'*+\\-./:<=>?@\\\\^`|~]+")), + GrammarToken( + "number", + compileHighlightPattern( + "[+-]?\\b(?:\\d(?:_?\\d)*(?:\\.\\d(?:_?\\d)*)?(?:[eE][+-]?\\d(?:_?\\d)*)?|0x[\\da-fA-F](?:_?[\\da-fA-F])*(?:\\.[\\da-fA-F](?:_?[\\da-fA-D])*)?(?:[pP][+-]?\\d(?:_?\\d)*)?)\\b|\\binf\\b|\\bnan(?::0x[\\da-fA-F](?:_?[\\da-fA-D])*)?\\b")), + GrammarToken("punctuation", compileHighlightPattern("[()]")), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("operator", compileHighlightPattern("=")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("\\.")), +]); diff --git a/lib/highlight/xml.dart b/lib/highlight/xml.dart new file mode 100644 index 0000000..370355a --- /dev/null +++ b/lib/highlight/xml.dart @@ -0,0 +1,89 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `xml`. +/// +/// Import this library only when you need `xml` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightXml { + /// The grammar for `xml`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "comment", compileHighlightPattern(""), + greedy: true), + GrammarToken("prolog", compileHighlightPattern("<\\?[\\s\\S]+?\\?>"), + greedy: true), + GrammarToken( + "doctype", + compileHighlightPattern( + "\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:[^<\"'\\]]|\"[^\"]*\"|'[^']*'|<(?!!--)|)*\\]\\s*)?>", + caseSensitive: false), + greedy: true, + inside: () => _g1), + GrammarToken( + "cdata", + compileHighlightPattern("", + caseSensitive: false), + greedy: true), + GrammarToken( + "tag", + compileHighlightPattern( + "<\\/?(?!\\d)[^\\s>\\/=\$<%]+(?:\\s(?:\\s*[^\\s>\\/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?\\s*\\/?>"), + greedy: true, + inside: () => _g2), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g1 = Grammar([ + GrammarToken("internal-subset", + compileHighlightPattern("(^[^\\[]*\\[)[\\s\\S]+(?=\\]>\$)"), + lookbehind: true, greedy: true, inside: () => _g0), + GrammarToken("string", compileHighlightPattern("\"[^\"]*\"|'[^']*'"), + greedy: true), + GrammarToken("punctuation", compileHighlightPattern("^\$|[[\\]]")), + GrammarToken( + "doctype-tag", compileHighlightPattern("^DOCTYPE", caseSensitive: false)), + GrammarToken("name", compileHighlightPattern("[^\\s<>'\"]+")), +]); + +final Grammar _g2 = Grammar([ + GrammarToken("tag", compileHighlightPattern("^<\\/?[^\\s>\\/]+"), + inside: () => _g3), + GrammarToken("attr-value", + compileHighlightPattern("=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+)"), + inside: () => _g4), + GrammarToken("punctuation", compileHighlightPattern("\\/?>")), + GrammarToken("attr-name", compileHighlightPattern("[^\\s>\\/]+"), + inside: () => _g5), +]); + +final Grammar _g3 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^<\\/?")), + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); + +final Grammar _g4 = Grammar([ + GrammarToken("punctuation", compileHighlightPattern("^="), + alias: "attr-equals"), + GrammarToken("punctuation", compileHighlightPattern("^(\\s*)[\"']|[\"']\$"), + lookbehind: true), + GrammarToken("entity", + compileHighlightPattern("&[\\da-z]{1,8};", caseSensitive: false), + alias: "named-entity"), + GrammarToken("entity", + compileHighlightPattern("&#x?[\\da-f]{1,8};", caseSensitive: false)), +]); + +final Grammar _g5 = Grammar([ + GrammarToken("namespace", compileHighlightPattern("^[^\\s>\\/:]+:")), +]); diff --git a/lib/highlight/yaml.dart b/lib/highlight/yaml.dart new file mode 100644 index 0000000..c13ab25 --- /dev/null +++ b/lib/highlight/yaml.dart @@ -0,0 +1,78 @@ +// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; + +/// Syntax grammar for `yaml`. +/// +/// Import this library only when you need `yaml` highlighting; unused +/// languages are dropped from the build. +abstract final class HighlightYaml { + /// The grammar for `yaml`. + static final Grammar grammar = _g0; +} + +final Grammar _g0 = Grammar([ + GrammarToken( + "scalar", + compileHighlightPattern( + "([\\-:]\\s*(?:\\s(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?[|>])[ \\t]*(?:((?:\\r?\\n|\\r)[ \\t]+)\\S[^\\r\\n]*(?:\\2[^\\r\\n]+)*)"), + lookbehind: true, + alias: "string"), + GrammarToken("comment", compileHighlightPattern("#.*")), + GrammarToken( + "key", + compileHighlightPattern( + "((?:^|[:\\-,[{\\r\\n?])[ \\t]*(?:(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?)(?:(?:[^\\s\\x00-\\x08\\x0e-\\x1f!\"#%&'*,\\-:>?@[\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-][^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff])(?:[ \\t]*(?:(?![#:])[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|:[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]))*|\"(?:[^\"\\\\\\r\\n]|\\\\.)*\"|'(?:[^'\\\\\\r\\n]|\\\\.)*')(?=\\s*:\\s)"), + lookbehind: true, + greedy: true, + alias: "atrule"), + GrammarToken( + "directive", compileHighlightPattern("(^[ \\t]*)%.+", multiLine: true), + lookbehind: true, alias: "important"), + GrammarToken( + "datetime", + compileHighlightPattern( + "([:\\-,[{]\\s*(?:\\s(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?)(?:\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \\t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \\t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?)(?=[ \\t]*(?:\$|,|\\]|\\}|(?:[\\r\\n]\\s*)?#))", + multiLine: true), + lookbehind: true, + alias: "number"), + GrammarToken( + "boolean", + compileHighlightPattern( + "([:\\-,[{]\\s*(?:\\s(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?)(?:false|true)(?=[ \\t]*(?:\$|,|\\]|\\}|(?:[\\r\\n]\\s*)?#))", + caseSensitive: false, + multiLine: true), + lookbehind: true, + alias: "important"), + GrammarToken( + "null", + compileHighlightPattern( + "([:\\-,[{]\\s*(?:\\s(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?)(?:null|~)(?=[ \\t]*(?:\$|,|\\]|\\}|(?:[\\r\\n]\\s*)?#))", + caseSensitive: false, + multiLine: true), + lookbehind: true, + alias: "important"), + GrammarToken( + "string", + compileHighlightPattern( + "([:\\-,[{]\\s*(?:\\s(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?)(?:\"(?:[^\"\\\\\\r\\n]|\\\\.)*\"|'(?:[^'\\\\\\r\\n]|\\\\.)*')(?=[ \\t]*(?:\$|,|\\]|\\}|(?:[\\r\\n]\\s*)?#))", + multiLine: true), + lookbehind: true, + greedy: true), + GrammarToken( + "number", + compileHighlightPattern( + "([:\\-,[{]\\s*(?:\\s(?:!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?(?:[ \t]+[*&][^\\s[\\]{},]+)?|[*&][^\\s[\\]{},]+(?:[ \t]+!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?)?)[ \\t]+)?)(?:[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan))(?=[ \\t]*(?:\$|,|\\]|\\}|(?:[\\r\\n]\\s*)?#))", + caseSensitive: false, + multiLine: true), + lookbehind: true), + GrammarToken( + "tag", + compileHighlightPattern( + "!(?:<[\\w\\-%#;/?:@&=+\$,.!~*'()[\\]]+>|(?:[a-zA-Z\\d-]*!)?[\\w\\-%#;/?:@&=+\$.~*'()]+)?")), + GrammarToken("important", compileHighlightPattern("[*&][^\\s[\\]{},]+")), + GrammarToken( + "punctuation", compileHighlightPattern("---|[:[\\]{}\\-,|>?]|\\.\\.\\.")), +]); diff --git a/lib/src/highlight/engine.dart b/lib/src/highlight/engine.dart new file mode 100644 index 0000000..192d646 --- /dev/null +++ b/lib/src/highlight/engine.dart @@ -0,0 +1,288 @@ +import 'package:flutter/painting.dart'; + +/// {@template highlight_grammar} +/// A syntax grammar: an ordered list of [GrammarToken] rules applied to source +/// text to produce styled spans. +/// +/// Grammars are tree-shakeable: each language lives in its own library (e.g. +/// `highlight/dart.dart`) exposing a single grammar, so importing one never +/// references the others and unused languages are dropped by the compiler. +/// {@endtemplate} +final class Grammar { + /// {@macro highlight_grammar} + Grammar(this.tokens, {Grammar Function()? rest}) : _rest = rest; + + /// The token rules, applied in order. Earlier rules claim their text first + /// (this ordering is what lets strings and comments swallow characters that + /// later rules would otherwise match). + final List tokens; + + final Grammar Function()? _rest; + + /// An additional grammar whose rules are applied after [tokens] (used by + /// templating languages to fall back to a host grammar). Stored as a thunk so + /// it can reference other languages without cycles. + Grammar? get rest => _rest?.call(); +} + +/// Compiles a highlighting pattern. If the source uses a regex feature Dart's +/// engine rejects, returns a never-matching pattern so a single bad rule +/// disables itself instead of breaking the whole language. +RegExp compileHighlightPattern( + String source, { + bool caseSensitive = true, + bool multiLine = false, + bool dotAll = false, + bool unicode = false, +}) { + try { + return RegExp( + source, + caseSensitive: caseSensitive, + multiLine: multiLine, + dotAll: dotAll, + unicode: unicode, + ); + } on FormatException { + return _never; + } +} + +/// A pattern that matches nothing (no character is both `\s` and `\S`). +final RegExp _never = RegExp(r'[^\s\S]'); + +/// {@template highlight_grammar_token} +/// One rule of a [Grammar]: a [pattern] whose matches are tagged with [type] +/// (and optionally re-tagged via [alias]), with optional nested highlighting +/// via [inside]. +/// {@endtemplate} +final class GrammarToken { + /// {@macro highlight_grammar_token} + GrammarToken( + this.type, + this.pattern, { + this.lookbehind = false, + this.greedy = false, + this.alias, + Grammar Function()? inside, + }) : _inside = inside; + + /// The token type (the grammar key), used to look up a style in the theme. + final String type; + + /// The pattern whose matches become tokens of this [type]. + final RegExp pattern; + + /// A "lookbehind" flag: when `true`, capture group 1 of [pattern] is treated + /// as an unstyled prefix and excluded from the emitted token (it is *not* a + /// regex look-behind assertion). + final bool lookbehind; + + /// A "greedy" flag, retained for fidelity. The current tokenizer relies on + /// grammar ordering rather than greedy re-scanning, so this is advisory. + final bool greedy; + + /// An alternative type name that takes precedence over [type] when resolving + /// a style (e.g. Dart's `metadata` is aliased to `function`). + final String? alias; + + final Grammar Function()? _inside; + + /// The nested grammar used to tokenize this rule's matched text, or `null`. + /// + /// Stored as a thunk so grammars can reference themselves or each other + /// without initialization cycles. + Grammar? get inside => _inside?.call(); +} + +/// A theme that maps token types to text styles for code highlighting. +/// +/// Implemented as a `switch` per theme (rather than a shared map or enum) so an +/// unused theme is dropped entirely by the compiler. +abstract interface class CodeHighlightTheme { + /// The background color for the code block surface, or `null` to keep the + /// ambient Markdown surface color. + Color? get background; + + /// The default foreground color for untokenized code, or `null` to keep the + /// ambient text color. Needed so a dark theme stays legible when the ambient + /// Markdown text color is dark. + Color? get foreground; + + /// The style for a given [tokenType], or `null` to inherit the base style. + TextStyle? styleFor(String tokenType); +} + +/// Turns the text of a fenced code block into styled [InlineSpan]s. +/// +/// Assign an instance to [MarkdownThemeData.highlighter] to enable syntax +/// highlighting. The default (no highlighter) paints code as plain text. +abstract interface class SyntaxHighlighter { + /// Returns the spans for [code], tagged for [language]. Implementations must + /// preserve text exactly: the concatenation of the returned spans' text must + /// equal [code], so selection and copy stay aligned. [baseStyle] is the code + /// block's base (monospace) style, already applied to the enclosing span. + List highlight( + String code, String? language, TextStyle baseStyle); + + /// The background color to use for [language]'s code block, or `null` to keep + /// the ambient surface color. + Color? backgroundFor(String? language); + + /// The base style for [language]'s code, derived from [fallback] (the code + /// block's monospace style). Used to apply the theme's default foreground so + /// untokenized code stays legible on the theme background. + TextStyle baseStyleFor(String? language, TextStyle fallback); +} + +/// A [SyntaxHighlighter] driven by [Grammar]s. +/// +/// The caller supplies the exact set of [languages] to support, so only the +/// grammars named here are retained; every other language is dropped by +/// tree-shaking. +/// +/// ```dart +/// MarkdownHighlighter( +/// languages: {'dart': HighlightDart.grammar, 'json': HighlightJson.grammar}, +/// theme: HighlightThemes.githubDark, +/// ) +/// ``` +final class MarkdownHighlighter implements SyntaxHighlighter { + /// Creates a highlighter for the given [languages], styled by [theme]. + /// + /// [languages] keys are matched case-insensitively against the fenced code + /// block's language tag; unknown languages fall back to plain text. + MarkdownHighlighter({ + required Map languages, + required this.theme, + }) : _languages = { + for (final MapEntry(:key, :value) + in languages.entries) + key.toLowerCase(): value, + }; + + final Map _languages; + + /// The theme used to color tokens. + final CodeHighlightTheme theme; + + @override + Color? backgroundFor(String? language) => theme.background; + + @override + TextStyle baseStyleFor(String? language, TextStyle fallback) => + theme.foreground == null + ? fallback + : fallback.copyWith(color: theme.foreground); + + @override + List highlight( + String code, + String? language, + TextStyle baseStyle, + ) { + final grammar = + language == null ? null : _languages[language.toLowerCase()]; + if (grammar == null) return [TextSpan(text: code)]; + return _spansFor(_tokenize(code, grammar), theme); + } +} + +// =========================================================================== +// Tokenizer: an ordered-rule matcher. Each rule partitions the remaining plain +// text into typed spans; earlier rules (strings, comments) claim their text +// first, and nested `inside`/`rest` grammars are applied recursively. +// =========================================================================== + +/// A node of the tokenized tree: either raw [_Text] or a typed [_Span]. +sealed class _Node {} + +final class _Text extends _Node { + _Text(this.text); + final String text; +} + +final class _Span extends _Node { + _Span(this.type, this.alias, this.children); + final String type; + final String? alias; + final List<_Node> children; +} + +/// Guards against pathological self-referential grammars. +const int _maxDepth = 24; + +/// Tokenizes [text] against [grammar], returning a flat/nested node list whose +/// concatenated text equals [text] (it only partitions, never edits). +List<_Node> _tokenize(String text, Grammar grammar, [int depth = 0]) { + final nodes = <_Node>[_Text(text)]; + if (depth < _maxDepth) _applyGrammar(nodes, grammar, depth); + return nodes; +} + +/// Applies [grammar]'s rules (then its [Grammar.rest]) across [nodes] in place. +void _applyGrammar(List<_Node> nodes, Grammar grammar, int depth) { + for (final rule in grammar.tokens) { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + if (node is! _Text) continue; + final replacement = _applyRule(node.text, rule, depth); + if (replacement == null) continue; + nodes.replaceRange(i, i + 1, replacement); + // Skip the freshly inserted nodes; they must not be re-scanned by the + // same rule (already exhaustively matched) but will be seen by the next. + i += replacement.length - 1; + } + } + final rest = grammar.rest; + if (rest != null && depth < _maxDepth) _applyGrammar(nodes, rest, depth + 1); +} + +/// Applies a single [rule] to a plain [text] segment. Returns `null` when the +/// rule does not match (so the caller can leave the segment untouched). +List<_Node>? _applyRule(String text, GrammarToken rule, int depth) { + List<_Node>? out; + var pos = 0; + for (final match in rule.pattern.allMatches(text)) { + var start = match.start; + var matched = match.group(0)!; + if (rule.lookbehind) { + final lead = (match.groupCount >= 1 ? match.group(1) : null)?.length ?? 0; + start += lead; + matched = matched.substring(lead); + } + if (matched.isEmpty) continue; + out ??= <_Node>[]; + if (start > pos) out.add(_Text(text.substring(pos, start))); + final inside = rule.inside; + out.add(_Span( + rule.type, + rule.alias, + inside == null + ? <_Node>[_Text(matched)] + : _tokenize(matched, inside, depth + 1), + )); + pos = start + matched.length; + } + if (out == null) return null; + if (pos < text.length) out.add(_Text(text.substring(pos))); + return out; +} + +/// Converts the tokenized tree into [InlineSpan]s, resolving each token's style +/// from [theme]. Container spans carry no text of their own; children inherit +/// the enclosing style, so token colors compose down the tree. +List _spansFor(List<_Node> nodes, CodeHighlightTheme theme) { + final out = []; + for (final node in nodes) { + switch (node) { + case _Text(:final text): + out.add(TextSpan(text: text)); + case _Span(:final type, :final alias, :final children): + final style = (alias == null ? null : theme.styleFor(alias)) ?? + theme.styleFor(type); + out.add(TextSpan(style: style, children: _spansFor(children, theme))); + } + } + return out; +} diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 312190a..a52cdb0 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -787,6 +787,14 @@ bool _hasClosingBacktick(List codes, int length, int from) { /// [markerLen]) exists at or after [from]. A closer must be right-flanking /// (preceded by a non-space); underscore closers must also be at a word /// boundary. Escaped characters are skipped. +/// +/// Right-flanking is a property of the whole delimiter *run*, not the +/// individual marker character: the deciding character is the one immediately +/// before the run, skipping consecutive same-marker delimiters. We therefore +/// only accept a closer at the run boundary (`codes[j - 1] != ch`). A run +/// whose boundary is not right-flanking cannot close from any of its inner +/// delimiters either, so e.g. `a **bold ** x` (the closing `**` follows a +/// space) stays literal instead of degrading into a lopsided italic. bool _hasEmphasisCloser( List codes, int length, int from, int ch, int markerLen) { for (var j = from; j < length; j++) { @@ -797,10 +805,11 @@ bool _hasEmphasisCloser( if (codes[j] != ch) continue; if (markerLen == 2) { if (j + 1 < length && codes[j + 1] == ch) { - if (j > 0 && !_isInlineSpace(codes[j - 1])) return true; + if (j > 0 && !_isInlineSpace(codes[j - 1]) && codes[j - 1] != ch) + return true; j++; // Consume the delimiter pair. } - } else if (j > 0 && !_isInlineSpace(codes[j - 1])) { + } else if (j > 0 && !_isInlineSpace(codes[j - 1]) && codes[j - 1] != ch) { if (ch != 0x5F /* _ */) return true; // Underscore: also require a word boundary after the closer. final after = j + 1; @@ -1139,3 +1148,229 @@ List _parseInlineSpans(String text, {Map? math}) { // For now, it returns an empty list as a placeholder. return spans; } + +// ============================================================================= +// Streaming (incremental) parser +// ============================================================================= + +/// {@template streaming_markdown_parser} +/// Incremental Markdown parser for streaming sources such as LLM token output. +/// +/// A plain [MarkdownDecoder] re-parses the whole document on every call, so +/// feeding it an `N`-line message one token at a time costs `O(Nยฒ)`. This +/// parser keeps the accumulated source and only re-parses the still-growing +/// *tail* of the document: once a run of blocks is provably complete โ€” +/// terminated by a blank line and not sitting inside an open code fence โ€” it is +/// *frozen* and never parsed again. +/// +/// The result of [add] / [current] is always identical, block for block, to +/// `Markdown.fromString(everythingAddedSoFar)` (using the same [decoder]) โ€” the +/// incremental path is a pure performance optimization, never a behavioral one. +/// Blocks whose type still depends on input that has not arrived yet โ€” an +/// unterminated code fence, a table header still missing its delimiter row, a +/// list or block quote that might continue โ€” deliberately stay in the live tail +/// and are re-evaluated on every [add], so they never freeze into the wrong +/// shape. +/// +/// ```dart +/// final parser = StreamingMarkdownParser(); +/// llmTokenStream.listen((token) { +/// final markdown = parser.add(token); // cheap, incremental +/// setState(() => _message = markdown); +/// }); +/// ``` +/// +/// For a [Stream] of chunks, prefer the [MarkdownStreamParsing.toMarkdown] +/// extension, which wires an instance of this class into a transform. +/// +/// The parser is stateful and single-conversation: call [reset] to reuse the +/// instance for a new document. +/// {@endtemplate} +class StreamingMarkdownParser { + /// Creates a streaming parser. + /// + /// [decoder] performs the actual parsing of each tail; pass a configured + /// [MarkdownDecoder] (e.g. `MarkdownDecoder(inlineMath: true)`) to match the + /// options you would use with [Markdown.fromString]. Defaults to a plain + /// [MarkdownDecoder]. + /// {@macro streaming_markdown_parser} + StreamingMarkdownParser({MarkdownDecoder decoder = const MarkdownDecoder()}) + : _decoder = decoder; + + /// The decoder used to (re)parse frozen prefixes and the live tail. + final MarkdownDecoder _decoder; + + /// Source of the frozen prefix (`== ` everything up to [_stableOffset]). Kept + /// as a materialized string so [current] can expose the full source without + /// re-joining lines; only grows when a prefix is frozen. + String _frozen = ''; + + /// The still-mutable remainder of the source (everything after the frozen + /// prefix). Re-parsed on every [add]. + String _tail = ''; + + /// Blocks parsed from [_frozen]. Never re-parsed once appended. + final List _stableBlocks = []; + + /// Blocks parsed from the current [_tail]. Rebuilt on every [add]. + List _tailBlocks = const []; + + /// The full Markdown source accumulated so far. + String get source => _tail.isEmpty ? _frozen : '$_frozen$_tail'; + + /// Number of blocks already frozen (exposed for diagnostics / benchmarks). + int get stableBlockCount => _stableBlocks.length; + + /// Appends a chunk of streamed Markdown text and returns the updated + /// [Markdown]. The chunk may end mid-line; the partial line is buffered and + /// completed by later chunks. + Markdown add(String chunk) { + if (chunk.isNotEmpty) { + _tail = _tail.isEmpty ? chunk : '$_tail$chunk'; + _freezeCompletedPrefix(); + _tailBlocks = + _tail.isEmpty ? const [] : _decoder.convert(_tail).blocks; + } + return current; + } + + /// The current parsed [Markdown] (frozen prefix + live tail) without adding + /// anything. Equivalent, block for block, to `Markdown.fromString(source)`. + Markdown get current => _build(); + + /// Freezes any newly-completed leading blocks so they are never re-parsed. + /// + /// Finds the largest safe boundary in [_tail] (via [_safeCutOffset]), + /// converts the prefix up to it once, moves it into [_frozen] / + /// [_stableBlocks], and leaves the remainder as the live tail. + void _freezeCompletedPrefix() { + final cut = _safeCutOffset(_tail); + if (cut <= 0) return; + final prefix = _tail.substring(0, cut); + // Safe by construction: [cut] sits at a hard block boundary outside any + // open fence, so converting the prefix in isolation yields exactly the + // blocks it would contribute to a full-document parse. + _stableBlocks.addAll(_decoder.convert(prefix).blocks); + _frozen = _frozen.isEmpty ? prefix : '$_frozen$prefix'; + _tail = _tail.substring(cut); + } + + /// Resets the parser to its initial empty state so the instance can be reused + /// for a new document. + void reset() { + _frozen = ''; + _tail = ''; + _stableBlocks.clear(); + _tailBlocks = const []; + } + + /// Builds the combined [Markdown] view (frozen prefix + live tail). + Markdown _build() { + final src = source; + if (src.isEmpty) return const Markdown.empty(); + final blocks = [..._stableBlocks, ..._tailBlocks]; + return Markdown( + markdown: src, + blocks: List.unmodifiable(blocks), + ); + } +} + +/// Returns the offset within [tail] at which the still-mutable remainder of the +/// document begins: everything before it forms complete blocks that no future +/// input can change and may be frozen. Returns `0` when nothing new can be +/// frozen yet. +/// +/// A cut is placed at the start of a **complete, non-blank line whose preceding +/// complete line is blank**, provided that position is not inside an open code +/// fence. Because this block grammar has no lazy continuation across a blank +/// line (a blank line terminates paragraphs, quotes, lists and tables, and only +/// an open code fence swallows blanks), such a position is a hard boundary: +/// parsing the prefix and the suffix separately yields exactly the same blocks +/// as parsing the whole. The last such boundary in [tail] is returned so as +/// much as possible is frozen. +int _safeCutOffset(String tail) { + final len = tail.length; + var cut = 0; + var inFence = false; + var fence = + 0; // active fence marker code unit (0x60 ``` / 0x7E ~~~), 0 = none + var prevBlank = false; // the previous *complete* line was blank + var havePrev = false; // there is a previous complete line + var start = 0; + + for (var i = 0; i <= len; i++) { + // A line ends at each '\n' and at end-of-string. The final segment + // (i == len) is the still-growing pending line and is never itself frozen. + if (i != len && tail.codeUnitAt(i) != 0x0A /* \n */) continue; + final complete = i < len; + + // Evaluate a cut at this line's start, using the fence/blank state that + // holds *before* this line is folded in. + if (havePrev && prevBlank && !inFence && !_segIsBlank(tail, start, i)) { + cut = start; + } + + if (complete) { + final marker = _fenceMarker(tail, start, i); + if (!inFence) { + if (marker != 0) { + inFence = true; + fence = marker; + } + } else if (marker == fence) { + inFence = false; + fence = 0; + } + prevBlank = _segIsBlank(tail, start, i); + havePrev = true; + start = i + 1; + } + } + return cut; +} + +/// Whether the line segment `s[start..end)` is blank โ€” only spaces, tabs, or a +/// trailing carriage return (so CRLF blank lines are recognized). Mirrors +/// [MarkdownDecoder]'s blank-line rule for the streaming boundary scan. +bool _segIsBlank(String s, int start, int end) { + for (var k = start; k < end; k++) { + final c = s.codeUnitAt(k); + if (c != 0x20 /* space */ && c != 0x09 /* tab */ && c != 0x0D /* CR */) { + return false; + } + } + return true; +} + +/// Returns the fence marker code unit (0x60 for ```` ``` ````, 0x7E for `~~~`) +/// when `s[start..end)` opens or closes a fenced code block, else `0`. Matches +/// the `startsWith('```') || startsWith('~~~')` test in [MarkdownDecoder]. +int _fenceMarker(String s, int start, int end) { + if (end - start < 3) return 0; + final c = s.codeUnitAt(start); + if (c != 0x60 /* ` */ && c != 0x7E /* ~ */) return 0; + return (s.codeUnitAt(start + 1) == c && s.codeUnitAt(start + 2) == c) ? c : 0; +} + +/// Incremental Markdown parsing for a stream of text chunks. +extension MarkdownStreamParsing on Stream { + /// Parses this stream of Markdown text chunks incrementally, emitting the + /// growing [Markdown] after each chunk. + /// + /// A single [StreamingMarkdownParser] is threaded through the stream, so + /// closed blocks are parsed once and only the live tail is re-parsed as + /// tokens arrive. Pass a configured [decoder] (e.g. for `inlineMath`) to + /// match [Markdown.fromString]. The last emitted value equals + /// `Markdown.fromString(concatenationOfAllChunks)`. + /// + /// ```dart + /// llmTokenStream.toMarkdown().listen((md) => setState(() => _message = md)); + /// ``` + Stream toMarkdown({ + MarkdownDecoder decoder = const MarkdownDecoder(), + }) { + final parser = StreamingMarkdownParser(decoder: decoder); + return map(parser.add); + } +} diff --git a/lib/src/render.dart b/lib/src/render.dart index a8848b2..8e9d5fd 100644 --- a/lib/src/render.dart +++ b/lib/src/render.dart @@ -1,1730 +1,21 @@ -//ignore_for_file: unnecessary_import - -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:meta/meta.dart' as meta show internal; - -import 'markdown.dart'; -import 'nodes.dart'; -import 'theme.dart'; - -@meta.internal -class MarkdownRenderObject extends RenderBox { - MarkdownRenderObject({ - required Markdown markdown, - required MarkdownThemeData theme, - }) : _painter = MarkdownPainter( - markdown: markdown, - theme: theme, - ); - - /// Painter for rendering markdown content. - final MarkdownPainter _painter; - - /// Current size of the render box. - @override - Size get size => _size; - Size _size = Size.zero; - - @override - bool get isRepaintBoundary => false; - - @override - bool get alwaysNeedsCompositing => false; - - @override - bool get sizedByParent => false; - - @override - set size(Size value) { - final prev = super.hasSize ? super.size : null; - super.size = value; - if (prev == value) return; - _size = value; - } - - @override - void debugResetSize() { - super.debugResetSize(); - if (!super.hasSize) return; - _size = super.size; - } - - @override - Size computeDryLayout(BoxConstraints constraints) => - constraints.constrain(_painter.layout(maxWidth: constraints.maxWidth)); - - @override - void performLayout() { - // Set the size of the render box to match the painter's size. - size = - constraints.constrain(_painter.layout(maxWidth: constraints.maxWidth)); - } - - @override - // ignore: unnecessary_overrides - void performResize() { - size = computeDryLayout(constraints); - } - - @override - bool hitTestSelf(Offset position) => true; - - @override - bool hitTestChildren( - BoxHitTestResult result, { - required Offset position, - }) => - false; - - @override - bool hitTest(BoxHitTestResult result, {required Offset position}) { - var hitTarget = false; - if (size.contains(position)) { - hitTarget = hitTestSelf(position); - result.add(BoxHitTestEntry(this, position)); - } - return hitTarget; - } - - @override - void handleEvent(PointerEvent event, BoxHitTestEntry entry) { - _painter.handleEvent(event); - } - - /// Handles system font changes by marking the render object as needing layout - void _handleSystemFontsChange() { - // Invalidate cached layouts in painter and all block painters - _painter.invalidateLayout(); - // Request new layout and paint - markNeedsLayout(); - } - - @override - // ignore: unnecessary_overrides - void attach(PipelineOwner owner) { - super.attach(owner); - PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange); - } - - /// Updates the render object with a new values. - /// This method should be called whenever the markdown or theme changes. - @meta.internal - void update({ - required Markdown markdown, - required MarkdownThemeData theme, - }) { - if (_painter.update( - markdown: markdown, - theme: theme, - )) { - // Mark the render object as needing layout. - markNeedsLayout(); - } - } - - @override - @protected - void detach() { - PaintingBinding.instance.systemFonts - .removeListener(_handleSystemFontsChange); - super.detach(); - } - - @override - @protected - void dispose() { - super.dispose(); - _painter.dispose(); - } - - @override - @protected - void paint(PaintingContext context, Offset offset) { - if (_painter.isEmpty) - return; // If the markdown is empty, do not paint anything. - - // ignore: unused_local_variable - final canvas = context.canvas - ..save() - ..translate(offset.dx, offset.dy); - //..clipRect(Rect.fromLTWH(0, 0, size.width, size.height)); - - _painter.paint(canvas, size); - - canvas.restore(); - } -} - -/// A painter for rendering markdown content via blocks and spans. -@meta.internal -class MarkdownPainter { - /// Creates a [MarkdownPainter] instance. - MarkdownPainter({ - required Markdown markdown, - required MarkdownThemeData theme, - }) : _markdown = markdown, - _theme = theme, - _isEmpty = markdown.isEmpty, - _size = Size.zero { - _rebuild(); - } - - /// Is the markdown entity empty? - bool get isEmpty => _isEmpty; - bool _isEmpty; - - /// Current markdown entity to render. - Markdown _markdown; - - /// Current theme for the markdown widget. - MarkdownThemeData _theme; - - /// The size of the painted markdown content. - Size get size => _size; - Size _size; - - /// Indicates if the layout needs to be recalculated. - bool _needsLayout = true; - - Float32List _blockOffsets = Float32List(0); - List _blockPainters = const []; - - static BlockPainter _defaultBlockBuilder( - MD$Block block, - MarkdownThemeData theme, - ) => - block.map( - paragraph: (p) => BlockPainter$Paragraph( - spans: p.spans, - theme: theme, - ), - heading: (h) => BlockPainter$Heading( - level: h.level, - spans: h.spans, - theme: theme, - ), - quote: (q) => BlockPainter$Quote( - spans: q.spans, - indent: q.indent, - theme: theme, - ), - code: (c) => BlockPainter$Code( - language: c.language, - text: c.text, - theme: theme, - ), - list: (l) => BlockPainter$List( - items: l.items, - theme: theme, - ), - divider: (d) => BlockPainter$Divider( - theme: theme, - ), - table: (t) => BlockPainter$Table( - header: t.header, - rows: t.rows, - alignments: t.alignments, - theme: theme, - ), - alert: (a) => BlockPainter$Alert( - alert: a.alert, - spans: a.spans, - theme: theme, - ), - spacer: (s) => BlockPainter$Spacer( - count: s.count, - theme: theme, - ), - ); - - /// Rebuilds the block painters from the markdown blocks. - /// This method is called whenever the markdown or theme changes. - void _rebuild() { - _needsLayout = true; // Mark that layout needs to be recalculated. - _size = Size.zero; // Reset size before rebuilding. - final filter = _theme.blockFilter; - final filtered = - filter != null ? _markdown.blocks.where(filter) : _markdown.blocks; - final builder = _theme.builder ?? _defaultBlockBuilder; - _blockPainters = filtered - .map( - (block) => - builder(block, _theme) ?? _defaultBlockBuilder(block, _theme), - ) - .toList(growable: false); - _blockOffsets = Float32List(_blockPainters.length); - } - - /// Update the painter with new values. - /// If the values are the same, - /// no update is required and the method returns false. - bool update({ - required Markdown markdown, - required MarkdownThemeData theme, - }) { - if (identical(_markdown, markdown) && identical(_theme, theme)) - return false; - _lastSize = null; - _lastPicture = null; - _markdown = markdown; - _theme = theme; - _isEmpty = markdown.isEmpty; - _rebuild(); - return true; // Indicate that the painter was updated. - } - - /// Invalidate cached layouts when system fonts change. - /// This forces TextPainters to recreate their layouts with new fonts. - void invalidateLayout() { - _needsLayout = true; - _lastSize = null; - _lastPicture = null; - // Dispose and rebuild all block painters to recreate TextPainters - // with the new system fonts - for (final painter in _blockPainters) { - painter.dispose(); - } - _rebuild(); - } - - /// Layouts the markdown content with the given width. - Size layout({required double maxWidth}) { - if (_isEmpty) { - _size = Size.zero; - _needsLayout = false; // No need to layout if the markdown is empty. - return _size; // If the markdown is empty, return zero size. - } - var width = .0, height = .0; - final blocks = _blockPainters; - if (_blockOffsets.length != blocks.length) { - // Resize the block sizes array - // if it does not match the number of painters. - _blockOffsets = Float32List(blocks.length); - } - final offsets = _blockOffsets; - for (var i = 0; i < blocks.length; i++) { - offsets[i] = height; - final block = blocks[i]; - final size = block.layout(maxWidth); - width = math.max(width, size.width); - height += size.height; - } - _needsLayout = false; // No need to layout if the markdown is empty. - return _size = Size(width, height); - } - - /// Get the painter from the array by the vertical local position (dy). - /* static BlockPainter? _getPainterByHeight( - Iterable painters, - double dy, - ) { - var offset = .0; - BlockPainter? result; - for (var painter in painters) { - if (dy < offset) break; - result = painter; - offset += painter.size.height; // Update the offset for the next block. - } - return result; - } */ - - void handleEvent(PointerEvent event) { - if (_blockPainters.isEmpty) return; - // event.buttons, event.kind, event.position - // event.localPosition, event.delta, event.down - - // Only handle pointer down events for now. - // You can extend this to handle other pointer events if needed. - if (event is! PointerDownEvent && event is! PointerUpEvent) return; - - final pos = event.localPosition; - { - // Binary search to find the block painter by the vertical position. - final dy = pos.dy; - var min = 0; - var max = _blockPainters.length; - var idx = 0; - while (min < max) { - final mid = min + ((max - min) >> 1); - final offset = _blockOffsets[mid]; - //final comp = offset.compareTo(dy); - var comp = 0; - if (offset > dy) { - // The offset is greater than the position. - comp = 1; - } else { - idx = mid; // Remember the index of the block painter. - // The offset is less than or equal to the position. - comp = offset < dy ? -1 : 0; - } - if (comp == 0) { - break; // Found the exact match. - } else if (comp < 0) { - min = mid + 1; - } else { - max = mid; - } - } - switch (event) { - case PointerDownEvent(): - final blockTapEvent = PointerDownEvent( - // Adjust the position by the block offset. - position: Offset( - pos.dx, - pos.dy - _blockOffsets[idx], - ), - viewId: event.viewId, - timeStamp: event.timeStamp, - pointer: event.pointer, - kind: event.kind, - device: event.device, - buttons: event.buttons, - obscured: event.obscured, - pressure: event.pressure, - pressureMin: event.pressureMin, - pressureMax: event.pressureMax, - distanceMax: event.distanceMax, - size: event.size, - radiusMajor: event.radiusMajor, - radiusMinor: event.radiusMinor, - radiusMin: event.radiusMin, - radiusMax: event.radiusMax, - orientation: event.orientation, - tilt: event.tilt, - embedderId: event.embedderId, - ); - _blockPainters[idx].handleTapDown(blockTapEvent); - case PointerUpEvent(): - final blockTapEvent = PointerUpEvent( - // Adjust the position by the block offset. - position: Offset( - pos.dx, - pos.dy - _blockOffsets[idx], - ), - viewId: event.viewId, - timeStamp: event.timeStamp, - pointer: event.pointer, - kind: event.kind, - device: event.device, - buttons: event.buttons, - obscured: event.obscured, - pressure: event.pressure, - pressureMin: event.pressureMin, - pressureMax: event.pressureMax, - distanceMax: event.distanceMax, - size: event.size, - radiusMajor: event.radiusMajor, - radiusMinor: event.radiusMinor, - radiusMin: event.radiusMin, - radiusMax: event.radiusMax, - orientation: event.orientation, - tilt: event.tilt, - embedderId: event.embedderId, - ); - _blockPainters[idx].handleTapUp(blockTapEvent); - } - } - - // We can use the position to determine which block was hit. - //_getPainterByHeight(_blockPainters, pos.dy)?.handleEvent(event); - - // Handle taps for the links with urls. - /* switch (event) { - case PointerDownEvent(down: true): - // Handle pointer down events. - default: - // Handle other pointer events if needed. - break; - } */ - } - - /// The last size and picture used for painting. - /// This is used to avoid unnecessary recreation of the canvas picture. - /// If the size is the same as the last painted size, - Size? _lastSize; - - /// The last picture used for painting, - /// to avoid unnecessary recreation of the canvas picture. - /// If the size is the same as the last painted size, - /// we can reuse the last picture. - Picture? _lastPicture; - - /// The markdown content to paint. - void paint(Canvas canvas, Size size) { - assert( - !_needsLayout, - 'MarkdownPainter.paint() called without layout.', - ); - assert( - size.isFinite, - 'MarkdownPainter.paint() called with non-finite size: $size', - ); - - // Do not paint if the markdown is empty, - // or if the size is empty or infinite. - if (_isEmpty || size.isEmpty || size.isInfinite) return; - - if (_lastSize == size && _lastPicture != null) { - // If the size is the same as the last painted size, - // we can reuse the last picture. - canvas.drawPicture(_lastPicture!); - return; - } - - final recorder = PictureRecorder(); - final $canvas = Canvas(recorder); - - // Paint each block painter on the canvas. - var overflow = _size.height > size.height; - var offset = .0; - for (var painter in _blockPainters) { - if (overflow && offset > size.height) { - // If the painter's height exceeds the available height, - // we stop painting further blocks. - break; - } - painter.paint($canvas, size, offset); - offset += painter.size.height; // Update the offset for the next block. - } - - final picture = recorder.endRecording(); - canvas.drawPicture(picture); - _lastSize = size; - _lastPicture = picture; - } - - void dispose() { - _lastPicture?.dispose(); - _lastPicture = null; - for (final painter in _blockPainters) { - painter.dispose(); - } - _blockPainters = const []; - } -} - -/* InlineSpan _imageFromMarkdownSpan({ - required MD$Span span, - required MarkdownThemeData theme, -}) { - final url = span.extra?['url']; - if (url is! String || url.isEmpty) return const TextSpan(); - ImageProvider? provider; - if (url.startsWith('http://') || url.startsWith('https://')) { - provider = NetworkImage(url); - } else if (url.startsWith('asset://')) { - provider = AssetImage(Uri.parse(url).toFilePath()); - } else if (kIsWeb) { - provider = NetworkImage(url); - } else { - return const TextSpan(); - } - return WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: SizedBox.square( - dimension: 48, // Fixed size for the image. - child: Image( - image: provider, - width: 48, - height: 48, - filterQuality: FilterQuality.medium, - fit: BoxFit.scaleDown, - ), - ), - ); -} */ - -/// Builds a tap recognizer for the given markdown span. -TapGestureRecognizer? _buildTapRecognizer( - MD$Span span, - void Function(String title, String url)? onTap, -) { - if (onTap == null) return null; - if (span.extra case {'url': String url}) { - return TapGestureRecognizer() - ..onTap = () { - onTap(span.extra?['alt']?.toString() ?? span.text, url); - }; - } - return null; -} - -/// Helper function to create a [TextSpan] from markdown spans. -/// This function filters the spans based on the theme's span filter, -/// and applies the appropriate text style to each span. -TextSpan _paragraphFromMarkdownSpans({ - required Iterable spans, - required MarkdownThemeData theme, - TextStyle? textStyle, -}) { - final style = textStyle ?? theme.textStyle; - final spanFilter = theme.spanFilter; - final filtered = spanFilter != null ? spans.where(spanFilter) : spans; - final mapper = textStyle != null - ? (MD$Span span) { - return TextSpan( - text: span.text, - style: theme.textStyleFor(span.style).merge(style), - recognizer: span.style.contains(MD$Style.link) - ? _buildTapRecognizer(span, theme.onLinkTap) - : null, - ); - } - : (MD$Span span) { - return TextSpan( - text: span.text, - style: theme.textStyleFor(span.style), - recognizer: span.style.contains(MD$Style.link) - ? _buildTapRecognizer(span, theme.onLinkTap) - : null, - ); - }; - return TextSpan( - style: textStyle ?? theme.textStyle, - children: filtered.map(mapper).toList(growable: false), - ); -} - -/// A class for painting blocks in markdown. -/// You can implement this interface to create custom block painters. -abstract interface class BlockPainter { - /// The current size of the block. - /// Available only after [layout]. - abstract final Size size; - - /// Handle tap pointer down events for the block. - void handleTapDown(PointerDownEvent event); - - /// Handle tap pointer up events for the block. - void handleTapUp(PointerUpEvent event); - - /// Measure the block size with the given width. - Size layout(double width); - - /// Paint the block on the canvas at the given offset. - /// [canvas] is the canvas to paint on - /// [size] the whole size of the markdown content - /// [offset] is the vertical offset to paint the block at - void paint(Canvas canvas, Size size, double offset); - - /// Dispose all resources used by the painter. - void dispose(); -} - -@meta.internal -mixin ParagraphGestureHandler { - /// Handle tap events with a [TextPainter]. - @protected - InlineSpan? hitTestInlineSpanWithPointerEvent( - PointerEvent event, TextPainter painter) { - final pos = painter.getPositionForOffset(event.localPosition); - //final int index = pos.offset; - final span = painter.text?.getSpanForPosition(pos); - //final plainText = span?.toPlainText(); - //print('[${pos.offset}] $plainText'); - return span; - } -} - -/// A class for painting a paragraph block in markdown. -@meta.internal -class BlockPainter$Paragraph - with ParagraphGestureHandler - implements BlockPainter { - BlockPainter$Paragraph({ - required List spans, - required this.theme, - }) : painter = TextPainter( - text: _paragraphFromMarkdownSpans( - spans: spans, - theme: theme, - ), - textAlign: TextAlign.start, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ); - - final MarkdownThemeData theme; - - final TextPainter painter; - - @override - Size get size => _size; - Size _size = Size.zero; - - /// Last span hit by the tap down event. - TextSpan? _lastSpan; - - @override - void handleTapDown(PointerDownEvent event) { - _lastSpan = null; // Reset the span on tap down. - final span = hitTestInlineSpanWithPointerEvent(event, painter); - if (span case TextSpan textSpan) _lastSpan = textSpan; - } - - @override - void handleTapUp(PointerUpEvent event) { - if (_lastSpan == null) return; // No span was hit on tap down. - final span = hitTestInlineSpanWithPointerEvent(event, painter); - if (span != null && _lastSpan == span) { - // If the span is the same as the one hit on tap down, - // call the tap recognizer. - if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) - onTap?.call(); - } - _lastSpan = null; // Clear the span after handling the tap. - } - - @override - Size layout(double width) { - painter.layout( - minWidth: 0, - maxWidth: width, - ); - return _size = painter.size; - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // If the width is less than required do not paint anything. - if (size.width < _size.width) return; - painter.paint( - canvas, - Offset(0, offset), - ); - } - - @override - void dispose() { - painter.dispose(); - } -} - -/// A class for painting a paragraph block in markdown. -@meta.internal -class BlockPainter$Heading - with ParagraphGestureHandler - implements BlockPainter { - BlockPainter$Heading({ - required int level, - required List spans, - required this.theme, - }) : painter = TextPainter( - text: _paragraphFromMarkdownSpans( - spans: spans, - theme: theme, - textStyle: theme.headingStyleFor(level), - ), - textAlign: TextAlign.start, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ); - - final MarkdownThemeData theme; - - final TextPainter painter; - - @override - Size get size => _size; - Size _size = Size.zero; - - /// Last span hit by the tap down event. - TextSpan? _lastSpan; - - @override - void handleTapDown(PointerDownEvent event) { - _lastSpan = null; // Reset the span on tap down. - final span = hitTestInlineSpanWithPointerEvent(event, painter); - if (span case TextSpan textSpan) _lastSpan = textSpan; - } - - @override - void handleTapUp(PointerUpEvent event) { - if (_lastSpan == null) return; // No span was hit on tap down. - final span = hitTestInlineSpanWithPointerEvent(event, painter); - if (span != null && _lastSpan == span) { - // If the span is the same as the one hit on tap down, - // call the tap recognizer. - if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) - onTap?.call(); - } - _lastSpan = null; // Clear the span after handling the tap. - } - - @override - Size layout(double width) { - painter.layout( - minWidth: 0, - maxWidth: width, - ); - return _size = painter.size; - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // If the width is less than required do not paint anything. - if (size.width < _size.width) return; - painter.paint( - canvas, - Offset(0, offset), - ); - } - - @override - void dispose() { - painter.dispose(); - } -} - -/// A class for painting a quote block in markdown. -@meta.internal -class BlockPainter$Quote with ParagraphGestureHandler implements BlockPainter { - BlockPainter$Quote({ - required List spans, - required this.indent, - required this.theme, - }) : painter = TextPainter( - text: _paragraphFromMarkdownSpans( - spans: spans, - theme: theme, - textStyle: theme.quoteStyle ?? theme.textStyle, - ), - textAlign: TextAlign.start, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ), - linePaint = Paint() - ..color = theme.dividerColor ?? - const Color(0x7F7F7F7F) // Gray color for the line. - ..isAntiAlias = false - ..strokeWidth = 4.0 - ..style = PaintingStyle.fill; - - final MarkdownThemeData theme; - - final TextPainter painter; - - final int indent; // Indentation for quote blocks. - - static const double lineIndent = 10.0; // Indentation for quote blocks. - - final Paint linePaint; - - @override - Size get size => _size; - Size _size = Size.zero; - - /// Last span hit by the tap down event. - TextSpan? _lastSpan; - - @override - void handleTapDown(PointerDownEvent event) { - _lastSpan = null; // Reset the span on tap down. - final span = hitTestInlineSpanWithPointerEvent(event, painter); - if (span case TextSpan textSpan) _lastSpan = textSpan; - } - - @override - void handleTapUp(PointerUpEvent event) { - if (_lastSpan == null) return; // No span was hit on tap down. - final span = hitTestInlineSpanWithPointerEvent(event, painter); - if (span != null && _lastSpan == span) { - // If the span is the same as the one hit on tap down, - // call the tap recognizer. - if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) - onTap?.call(); - } - _lastSpan = null; // Clear the span after handling the tap. - } - - @override - Size layout(double width) { - // Adjust width for indentation. - painter.layout( - minWidth: 0, - maxWidth: math.max(width - lineIndent - indent * lineIndent, 0), - ); - return _size = Size( - painter.size.width + lineIndent + indent * lineIndent, - painter.size.height, - ); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // If the width is less than required do not paint anything. - if (size.width < _size.width) return; - - // --- Draw vertical lines --- // - for (var i = 1; i <= indent; i++) - canvas.drawLine( - Offset( - i * lineIndent, - offset, - ), - Offset( - i * lineIndent, - offset + _size.height, - ), - linePaint, - ); - - painter.paint( - canvas, - Offset( - lineIndent + indent * lineIndent, - offset, - ), - ); - } - - @override - void dispose() { - painter.dispose(); - } -} - -/// A class for painting a GitHub-style alert (admonition) block in markdown. -@meta.internal -class BlockPainter$Alert with ParagraphGestureHandler implements BlockPainter { - BlockPainter$Alert({ - required this.alert, - required List spans, - required this.theme, - }) : _accent = theme.alertColorFor(alert), - titlePainter = TextPainter( - text: TextSpan( - text: alert.title, - style: theme.textStyle.copyWith( - color: theme.alertColorFor(alert), - fontWeight: FontWeight.bold, - ), - ), - textAlign: TextAlign.start, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ), - bodyPainter = TextPainter( - text: _paragraphFromMarkdownSpans(spans: spans, theme: theme), - textAlign: TextAlign.start, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ); - - /// The kind of the alert being painted. - final MD$AlertType alert; - - final MarkdownThemeData theme; - - final Color _accent; - - /// Painter for the alert title (e.g. "Note"). - final TextPainter titlePainter; - - /// Painter for the alert body content. - final TextPainter bodyPainter; - - static const double padding = 10.0; - static const double barWidth = 4.0; - static const double gap = 10.0; - static const double titleGap = 4.0; - - /// Left offset where the title/body content begins. - double get _contentLeft => padding + barWidth + gap; - - @override - Size get size => _size; - Size _size = Size.zero; - - /// Whether the body has any content to paint. - bool _hasBody = false; - - /// Last span hit by the tap down event. - TextSpan? _lastSpan; - - Offset get _bodyOrigin => - Offset(_contentLeft, padding + titlePainter.height + titleGap); - - TextSpan? _spanForPosition(Offset localPosition) { - if (!_hasBody) return null; - final local = localPosition - _bodyOrigin; - if (local.dx < 0 || - local.dy < 0 || - local.dx > bodyPainter.width || - local.dy > bodyPainter.height) return null; - final position = bodyPainter.getPositionForOffset(local); - final span = bodyPainter.text?.getSpanForPosition(position); - return span is TextSpan ? span : null; - } - - @override - void handleTapDown(PointerDownEvent event) { - _lastSpan = _spanForPosition(event.localPosition); - } - - @override - void handleTapUp(PointerUpEvent event) { - if (_lastSpan == null) return; - final span = _spanForPosition(event.localPosition); - if (span != null && _lastSpan == span) { - if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) - onTap?.call(); - } - _lastSpan = null; - } - - @override - Size layout(double width) { - final available = math.max(0.0, width - _contentLeft - padding); - titlePainter.layout(minWidth: 0, maxWidth: available); - bodyPainter.layout(minWidth: 0, maxWidth: available); - _hasBody = bodyPainter.text?.toPlainText().isNotEmpty ?? false; - - final contentWidth = math.max( - titlePainter.width, - _hasBody ? bodyPainter.width : 0.0, - ); - final contentHeight = - titlePainter.height + (_hasBody ? titleGap + bodyPainter.height : 0.0); - return _size = Size( - _contentLeft + contentWidth + padding, - contentHeight + padding * 2, - ); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - if (size.width < _size.width) return; - - final rect = Rect.fromLTWH(0, offset, size.width, _size.height); - // Tinted background. - canvas.drawRRect( - RRect.fromRectAndRadius(rect, const Radius.circular(6.0)), - Paint() - ..color = _accent.withValues(alpha: 0.10) - ..style = PaintingStyle.fill, - ); - // Accent bar on the left. - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(0, offset, barWidth, _size.height), - const Radius.circular(barWidth / 2), - ), - Paint() - ..color = _accent - ..style = PaintingStyle.fill, - ); - - titlePainter.paint(canvas, Offset(_contentLeft, offset + padding)); - if (_hasBody) { - bodyPainter.paint(canvas, _bodyOrigin + Offset(0, offset)); - } - } - - @override - void dispose() { - titlePainter.dispose(); - bodyPainter.dispose(); - } -} - -/// A helper class to store layout information for a single list item. -class _ListItemMetrics { - _ListItemMetrics({ - required this.bulletPainter, - required this.contentPainter, - required this.offset, - }); - - final TextPainter bulletPainter; - final TextPainter contentPainter; - final Offset offset; - - late final double height = - math.max(bulletPainter.height, contentPainter.height); - late final Size size = - Size(bulletPainter.width + contentPainter.width, height); - - void dispose() { - bulletPainter.dispose(); - contentPainter.dispose(); - } -} - -/// A class for painting a list block in markdown. -@meta.internal -class BlockPainter$List with ParagraphGestureHandler implements BlockPainter { - BlockPainter$List({ - required List items, - required this.theme, - }) : _items = items, - _painters = <_ListItemMetrics>[]; - - final MarkdownThemeData theme; - final List _items; - final List<_ListItemMetrics> _painters; - - // Indentation for the entire list block. - static const double _baseIndent = 8.0; - - // Indentation for each level of nesting. - static const double _levelIndent = 16.0; - - @override - Size get size => _size; - Size _size = Size.zero; - - /// Last span hit by the tap down event. - InlineSpan? _lastSpan; - - InlineSpan? _getSpanForPosition(Offset localPosition) { - for (final metrics in _painters) { - final contentOffset = - metrics.offset + Offset(metrics.bulletPainter.width, 0); - final contentRect = contentOffset & metrics.contentPainter.size; - if (contentRect.contains(localPosition)) { - final painterPosition = localPosition - contentOffset; - final textPosition = - metrics.contentPainter.getPositionForOffset(painterPosition); - return metrics.contentPainter.text?.getSpanForPosition(textPosition); - } - } - return null; - } - - @override - void handleTapDown(PointerDownEvent event) { - _lastSpan = null; // Reset the span on tap down. - _lastSpan = _getSpanForPosition(event.localPosition); - } - - @override - void handleTapUp(PointerUpEvent event) { - if (_lastSpan == null) return; // No span was hit on tap down. - final newSpan = _getSpanForPosition(event.localPosition); - if (newSpan != null && _lastSpan == newSpan) { - if (newSpan - case TextSpan(recognizer: final TapGestureRecognizer recognizer)) { - recognizer.onTap?.call(); - } - } - - _lastSpan = null; // Clear the span after handling the tap. - } - - @override - Size layout(double width) { - for (final painter in _painters) { - painter.dispose(); - } - _painters.clear(); - - double currentHeight = 0; - double maxContentWidth = 0; - - void layoutItems(List items, int level) { - final indent = _baseIndent + level * _levelIndent; - for (final item in items) { - // Task-list items render a checkbox instead of a bullet/number. - final bulletText = switch (item.checked) { - true => 'โ˜‘', - false => 'โ˜', - null => switch (item.marker) { - '-' || '*' || '+' => 'โ€ข', - _ => item.marker, - }, - }; - final bulletPainter = TextPainter( - text: TextSpan(text: '$bulletText ', style: theme.textStyle), - textDirection: theme.textDirection, - textScaler: theme.textScaler, - )..layout(); - - final contentPainter = TextPainter( - text: _paragraphFromMarkdownSpans(spans: item.spans, theme: theme), - textDirection: theme.textDirection, - textScaler: theme.textScaler, - )..layout(maxWidth: math.max(0, width - indent - bulletPainter.width)); - - final metrics = _ListItemMetrics( - bulletPainter: bulletPainter, - contentPainter: contentPainter, - offset: Offset(indent, currentHeight), - ); - _painters.add(metrics); - - currentHeight += metrics.height; - maxContentWidth = - math.max(maxContentWidth, indent + metrics.size.width); - - if (item.children.isNotEmpty) { - layoutItems(item.children, level + 1); - } - } - } - - layoutItems(_items, 0); - return _size = Size(maxContentWidth, currentHeight); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - for (final metrics in _painters) { - final bulletOffset = metrics.offset + Offset(0, offset); - metrics.bulletPainter.paint(canvas, bulletOffset); - - final contentOffset = - bulletOffset + Offset(metrics.bulletPainter.width, 0); - metrics.contentPainter.paint(canvas, contentOffset); - } - } - - @override - void dispose() { - for (final metrics in _painters) { - metrics.dispose(); - } - _painters.clear(); - } -} - -/// A class for painting a spacer block in markdown. -@meta.internal -class BlockPainter$Spacer implements BlockPainter { - BlockPainter$Spacer({ - required this.count, - required this.theme, - }); - - final int count; - - final MarkdownThemeData theme; - - @override - Size get size => _size; - Size _size = Size.zero; - - @override - void handleTapDown(PointerDownEvent _) {/* Do nothing */} - - @override - void handleTapUp(PointerUpEvent _) {/* Do nothing */} - - @override - Size layout(double width) { - final height = theme.textStyle.fontSize ?? kDefaultFontSize; - return _size = Size(0, height * count); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // Do not paint anything - /* canvas.drawRect( - Rect.fromLTWH(0, offset, size.width, _size.height), - Paint()..color = theme.textStyle.color ?? const Color(0x00000000), - ); */ - } - - @override - void dispose() { - // Noting to dispose - } -} - -/// A class for painting a spacer block in markdown. -@meta.internal -class BlockPainter$Divider implements BlockPainter { - BlockPainter$Divider({ - required this.theme, - }) : _paint = Paint() - ..color = theme.textStyle.color ?? const Color(0xFF000000) - ..isAntiAlias = false - ..strokeWidth = 1.0 - ..style = PaintingStyle.fill; - - final Paint _paint; - final MarkdownThemeData theme; - - @override - Size get size => _size; - Size _size = Size.zero; - - @override - void handleTapDown(PointerDownEvent _) {/* Do nothing */} - - @override - void handleTapUp(PointerUpEvent _) {/* Do nothing */} - - @override - Size layout(double width) { - final height = theme.textStyle.fontSize ?? kDefaultFontSize; - return _size = Size(0, height); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // Draw a horizontal line across the width of the canvas. - final center = offset + _size.height / 2; - canvas.drawLine( - Offset(0, center), - Offset(size.width, center), - _paint, - ); - } - - @override - void dispose() { - // Noting to dispose - } -} - -/// A class for painting a code block in markdown. -@meta.internal -class BlockPainter$Code implements BlockPainter { - BlockPainter$Code({ - required String text, - required String? language, - required this.theme, - }) : painter = TextPainter( - text: TextSpan( - text: text, - style: theme.textStyle.copyWith( - fontFamily: 'monospace', - fontSize: theme.textStyle.fontSize ?? kDefaultFontSize, - ), - ), - textAlign: TextAlign.start, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ); - - static const double padding = 8.0; // Padding for code blocks. - - final MarkdownThemeData theme; - - final TextPainter painter; - - @override - Size get size => _size; - Size _size = Size.zero; - - @override - void handleTapDown(PointerDownEvent _) {/* Do nothing */} - - @override - void handleTapUp(PointerUpEvent _) {/* Do nothing */} - - @override - Size layout(double width) { - if (width <= padding * 2) { - // If the width is less than or equal to padding, return zero size. - _size = Size.zero; - return _size; - } - painter.layout( - minWidth: 0, - maxWidth: width - padding * 2, - ); - return _size = Size( - painter.size.width + padding * 2, // Add padding to the width. - painter.size.height + padding * 2, // Add padding to the height. - ); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // If the width is less than required do not paint anything. - if (size.width < _size.width) return; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(0, offset, size.width, _size.height), - const Radius.circular(padding), - ), - Paint() - ..color = theme.surfaceColor ?? const Color.fromARGB(255, 235, 235, 235) - ..isAntiAlias = false - ..style = PaintingStyle.fill, - ); - painter.paint( - canvas, - Offset(padding, offset + padding), - ); - } - - @override - void dispose() { - painter.dispose(); - } -} - -/// A class for painting a table block in markdown. -@meta.internal -class BlockPainter$Table with ParagraphGestureHandler implements BlockPainter { - BlockPainter$Table({ - required this.header, - required this.rows, - required this.theme, - this.alignments = const [], - }) : columns = header.cells.length, - _columnWidths = List.filled(header.cells.length, 0.0), - _rowHeights = List.filled(rows.length + 1, 0.0), - _borderPaint = Paint() - ..color = theme.dividerColor ?? const Color(0x1F000000) - ..style = PaintingStyle.stroke - ..isAntiAlias = false - ..strokeWidth = 1.0, - _rowBackgroundPaint = Paint() - ..style = PaintingStyle.fill - ..isAntiAlias = false - ..color = - theme.surfaceColor ?? const Color.fromARGB(255, 235, 235, 235); - - /// Padding for table cells. - static const double padding = 8.0; - - /// The theme for the markdown table. - final MarkdownThemeData theme; - - /// The number of columns in the table. - final int columns; - - /// The per-column alignment derived from the delimiter row. - final List alignments; - - /// Resolves the alignment for column [c], defaulting to - /// [MD$TableColumnAlign.none] when unspecified. - MD$TableColumnAlign _columnAlign(int c) => c >= 0 && c < alignments.length - ? alignments[c] - : MD$TableColumnAlign.none; - - /// The horizontal offset of a cell's text within its column, honoring the - /// column alignment (falling back to centered headers / left-aligned data). - double _cellHorizontalPadding(int r, int c, double painterWidth) => - switch (_columnAlign(c)) { - MD$TableColumnAlign.left => padding, - MD$TableColumnAlign.center => (_columnWidths[c] - painterWidth) / 2, - MD$TableColumnAlign.right => _columnWidths[c] - painterWidth - padding, - MD$TableColumnAlign.none => - (r == 0) ? (_columnWidths[c] - painterWidth) / 2 : padding, - }; - - final List _columnWidths; - final List _rowHeights; - final Paint _borderPaint; - final Paint _rowBackgroundPaint; - - Float32List? _borderPoints; - - /// The header row of the table. - final MD$TableRow header; - - /// The rows of the table. - final List rows; - - @override - Size get size => _size; - Size _size = Size.zero; - - List> _cellPainters = const []; - - /// Last span hit by the tap down event. - TextSpan? _lastSpan; - - @override - void handleTapDown(PointerDownEvent event) { - _lastSpan = null; // Reset the span on tap down. - final span = _getSpanForOffset(event.localPosition); - if (span != null) { - _lastSpan = span; - } - } - - @override - void handleTapUp(PointerUpEvent event) { - if (_lastSpan == null) return; // No span was hit on tap down. - final span = _getSpanForOffset(event.localPosition); - if (span != null && _lastSpan == span) { - // If the span is the same as the one hit on tap down, - // call the tap recognizer. - if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) - onTap?.call(); - } - _lastSpan = null; // Clear the span after handling the tap. - } - - TextSpan? _getSpanForOffset(Offset position) { - final rowHeights = - List.generate(_cellPainters.length, (r) => _rowHeights[r]); - - double currentY = 0.0; - - for (int r = 0; r < _cellPainters.length; r++) { - final rowHeight = rowHeights[r]; - double currentX = 0.0; - - if (position.dy >= currentY && position.dy < currentY + rowHeight) { - // In this row. - for (int c = 0; c < _cellPainters[r].length; c++) { - final painter = _cellPainters[r][c]; - if (painter.text == null) { - currentX += _columnWidths[c]; - continue; - } - final columnWidth = _columnWidths[c]; - - if (position.dx >= currentX && position.dx < currentX + columnWidth) { - // In this cell. - final verticalPadding = (rowHeight - painter.height) / 2; - final horizontalPadding = - _cellHorizontalPadding(r, c, painter.width); - - final painterOffset = Offset( - currentX + horizontalPadding, currentY + verticalPadding); - final localPosition = position - painterOffset; - - // Check if inside the actual painted text area. - if (localPosition.dx < 0 || - localPosition.dx > painter.width || - localPosition.dy < 0 || - localPosition.dy > painter.height) { - currentX += columnWidth; - continue; - } - - final textPosition = painter.getPositionForOffset(localPosition); - final span = painter.text!.getSpanForPosition(textPosition); - if (span is TextSpan) { - return span; - } - return null; // Found cell, but no span. - } - currentX += columnWidth; - } - } - currentY += rowHeight; - } - return null; - } - - @override - Size layout(double width) { - if (columns < 1) return _size = Size.zero; - - // Dispose old painters - for (final row in _cellPainters) { - for (final painter in row) { - painter.dispose(); - } - } - - final allRows = [header, ...rows]; - final naturalWidths = List.filled(columns, 0.0); - final minWidths = List.filled(columns, 0.0); - - // Create painters for each row and column and calculate natural widths - _cellPainters = List.generate(allRows.length, (r) { - final row = allRows[r]; - return List.generate(columns, (c) { - if (c >= row.cells.length) { - return TextPainter(textDirection: theme.textDirection); - } - final cell = row.cells[c]; - final style = (r == 0) - ? theme.textStyle.copyWith(fontWeight: FontWeight.bold) - : null; - final textPainter = TextPainter( - text: _paragraphFromMarkdownSpans( - spans: cell, theme: theme, textStyle: style), - textAlign: switch (_columnAlign(c)) { - MD$TableColumnAlign.left => TextAlign.left, - MD$TableColumnAlign.center => TextAlign.center, - MD$TableColumnAlign.right => TextAlign.right, - MD$TableColumnAlign.none => - (r == 0) ? TextAlign.center : TextAlign.start, - }, - textDirection: theme.textDirection, - textScaler: theme.textScaler, - ); - - // Calculate natural width - textPainter.layout(maxWidth: double.infinity); - naturalWidths[c] = - math.max(naturalWidths[c], textPainter.width + padding * 2); - - // Calculate min width (longest word) - final cellText = cell.map((s) => s.text).join(); - final words = cellText.split(RegExp(r'\s+')); - if (words.isNotEmpty) { - final longestWord = - words.reduce((a, b) => a.length > b.length ? a : b); - final wordPainter = TextPainter( - text: TextSpan(text: longestWord, style: style), - textDirection: theme.textDirection, - )..layout(); - minWidths[c] = - math.max(minWidths[c], wordPainter.width + padding * 2); - wordPainter.dispose(); - } - - return textPainter; - }); - }); - - _columnWidths.setAll(0, _distributeWidths(naturalWidths, minWidths, width)); - - final totalWidth = _columnWidths.reduce((a, b) => a + b); - - // Layout painters with final widths and calculate row heights - - double totalHeight = 0.0; - for (int r = 0; r < allRows.length; r++) { - double rowHeight = 0.0; - for (int c = 0; c < columns; c++) { - final painter = _cellPainters[r][c]; - if (painter.text == null) continue; - painter.layout(maxWidth: math.max(0.0, _columnWidths[c] - padding * 2)); - rowHeight = math.max( - rowHeight, - painter.height, - ); - } - - _rowHeights[r] = rowHeight + padding * 2; - totalHeight += _rowHeights[r]; - } - - // Cache border points - final points = Float32List(((allRows.length - 1) + (columns - 1)) * 4); - var pointIndex = 0; - // Horizontal lines - double lineY = 0; - for (int r = 0; r < allRows.length - 1; r++) { - lineY += _rowHeights[r]; - points[pointIndex++] = 0; - points[pointIndex++] = lineY; - points[pointIndex++] = totalWidth; - points[pointIndex++] = lineY; - } - // Vertical lines - double lineX = 0; - for (int c = 0; c < columns - 1; c++) { - lineX += _columnWidths[c]; - points[pointIndex++] = lineX; - points[pointIndex++] = 0; - points[pointIndex++] = lineX; - points[pointIndex++] = totalHeight; - } - _borderPoints = points; - - return _size = Size(totalWidth, totalHeight); - } - - @override - void paint(Canvas canvas, Size size, double offset) { - // If the width is less than required do not paint anything. - if (columns < 1) return; - - double currentY = offset; - final rowHeights = - List.generate(_cellPainters.length, (r) => _rowHeights[r]); - - for (int r = 0; r < _cellPainters.length; r++) { - double currentX = 0; - - // Draw background for even data rows. - if (r % 2 == 0 && r != 0) { - canvas.drawRect( - Rect.fromLTWH(0, currentY, _size.width, rowHeights[r]), - _rowBackgroundPaint, - ); - } - - for (int c = 0; c < columns; c++) { - final painter = _cellPainters[r][c]; - if (painter.text == null) { - currentX += _cellPainters[r].length > c ? _columnWidths[c] : 0; - continue; - } - - final verticalPadding = (rowHeights[r] - painter.height) / 2; - final horizontalPadding = _cellHorizontalPadding(r, c, painter.width); - - painter.paint( - canvas, - Offset( - currentX + horizontalPadding, - currentY + verticalPadding, - ), - ); - currentX += _columnWidths[c]; - } - currentY += rowHeights[r]; - } - - // Draw inner borders - if (_borderPoints != null) { - canvas.save(); - canvas.translate(0, offset); - canvas.drawRawPoints(PointMode.lines, _borderPoints!, _borderPaint); - canvas.restore(); - } - - // Draw outer borders - canvas.drawRect( - Rect.fromLTRB( - 0, - offset, - _size.width, - offset + _size.height, - ), - _borderPaint, - ); - } - - @override - void dispose() { - for (final row in _cellPainters) { - for (final painter in row) { - painter.dispose(); - } - } - _cellPainters = const []; - } - - /// Helper function to distribute widths among columns, respecting minimums. - /// If total minimum width exceeds availableWidth, - /// it returns the minimum widths as-is, - /// implying that the content will overflow and require scrolling. - List _distributeWidths( - List natural, List min, double availableWidth) { - final totalNatural = natural.reduce((a, b) => a + b); - final totalMin = min.reduce((a, b) => a + b); - - if (totalNatural <= availableWidth) { - return natural; - } - - if (totalMin <= availableWidth) { - final remainingSpace = availableWidth - totalMin; - final extraSpacePerColumn = [ - for (var i = 0; i < natural.length; i++) natural[i] - min[i] - ]; - final totalExtraSpace = extraSpacePerColumn.reduce((a, b) => a + b); - - if (totalExtraSpace <= 0.001) return min; - - return [ - for (var i = 0; i < natural.length; i++) - min[i] + remainingSpace * (extraSpacePerColumn[i] / totalExtraSpace) - ]; - } - return min; - } -} +/// Rendering layer for `flutter_md`. +/// +/// The block-painter framework, the default block painters, and the render +/// object / painter that drive them live under `render/`. This file re-exports +/// them so existing imports of `src/render.dart` keep resolving unchanged. +library; + +export 'render/block_painter.dart'; +export 'render/markdown_painter.dart'; +export 'render/markdown_render_object.dart'; +export 'render/span_builder.dart'; + +export 'render/blocks/alert.dart'; +export 'render/blocks/code.dart'; +export 'render/blocks/divider.dart'; +export 'render/blocks/heading.dart'; +export 'render/blocks/list.dart'; +export 'render/blocks/paragraph.dart'; +export 'render/blocks/quote.dart'; +export 'render/blocks/spacer.dart'; +export 'render/blocks/table.dart'; diff --git a/lib/src/render/block_painter.dart b/lib/src/render/block_painter.dart new file mode 100644 index 0000000..c261e09 --- /dev/null +++ b/lib/src/render/block_painter.dart @@ -0,0 +1,256 @@ +//ignore_for_file: unnecessary_import + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// A class for painting blocks in markdown. +/// You can implement this interface to create custom block painters. +abstract interface class BlockPainter { + /// The current size of the block. + /// Available only after [layout]. + abstract final Size size; + + /// Handle tap pointer down events for the block. + void handleTapDown(PointerDownEvent event); + + /// Handle tap pointer up events for the block. + void handleTapUp(PointerUpEvent event); + + /// Measure the block size with the given width. + Size layout(double width); + + /// Paint the block on the canvas at the given offset. + /// [canvas] is the canvas to paint on + /// [size] the whole size of the markdown content + /// [offset] is the vertical offset to paint the block at + void paint(Canvas canvas, Size size, double offset); + + /// Dispose all resources used by the painter. + void dispose(); +} + +/// A [BlockPainter] that supports text selection. All coordinates are local to +/// the block's top-left corner (as passed to [paint]'s `offset`). +/// +/// The [renderedText] should match [markdownBlockRenderedText] for the same +/// block so that hit-testing, highlighting, and model-side extraction agree on +/// the offset space. +/// +/// Caveat: a [MarkdownThemeData.spanFilter] that drops text-bearing spans +/// shifts the painter's offset space relative to the (unfiltered) model, so the +/// on-screen highlight stays correct but copied text may be misaligned. Avoid +/// dropping text-bearing spans when selection is enabled. +/// +/// Caveat: a [MarkdownThemeData.blockFilter] that drops whole blocks is never +/// highlighted on screen, but a selection spanning *across* a dropped block +/// still copies that hidden block's text โ€” selection extraction is model-based +/// and does not see render-time block filtering. Avoid `blockFilter` when +/// selection is enabled. +abstract interface class SelectableBlockPainter implements BlockPainter { + /// The block's rendered plain text. + String get renderedText; + + /// Maps a block-local [local] offset to a rendered-text index. + int offsetForLocalPosition(Offset local); + + /// Highlight rectangles (block-local) for the rendered range `[start, end)`. + List boxesForRange(int start, int end); + + /// The word range (in rendered-text space) at a block-local [local] point, + /// using the platform's word segmentation (`TextPainter.getWordBoundary`) so + /// double-click/tap selects real words (keeping intra-word punctuation such as + /// apostrophes, matching native text fields). + TextRange wordBoundaryForLocal(Offset local); + + /// Whether an actionable link (a span carrying a tap recognizer) sits under + /// the block-local [local] point โ€” used to show the click (hand) cursor. + bool isLinkAtLocal(Offset local); +} + +/// Provides [SelectableBlockPainter] for a block backed by a single +/// [TextPainter]. Subclasses supply [selectionPainter] and, when the glyphs are +/// not painted at the block origin, [selectionOrigin]. +mixin SelectableTextBlock implements SelectableBlockPainter { + /// The text painter that owns the selectable glyphs. + TextPainter get selectionPainter; + + /// Block-local origin where [selectionPainter] is painted. + Offset get selectionOrigin => Offset.zero; + + @override + String get renderedText => selectionPainter.plainText; + + @override + int offsetForLocalPosition(Offset local) => + selectionPainter.getPositionForOffset(local - selectionOrigin).offset; + + @override + List boxesForRange(int start, int end) => selectionPainter + .getBoxesForSelection(TextSelection(baseOffset: start, extentOffset: end)) + .map((box) => box.toRect().shift(selectionOrigin)) + .toList(growable: false); + + @override + TextRange wordBoundaryForLocal(Offset local) { + final offset = + selectionPainter.getPositionForOffset(local - selectionOrigin).offset; + return selectionPainter.getWordBoundary(TextPosition(offset: offset)); + } + + @override + bool isLinkAtLocal(Offset local) => + _spanHasRecognizerAt(selectionPainter, local - selectionOrigin); +} + +/// One selectable text run inside a multi-painter block (a list item or a table +/// cell): the [painter] that owns its glyphs, the block-local [origin] where it +/// is painted, and the [textStart] index of its text within the block's +/// [markdownBlockRenderedText] linearization. +class SelectableFragment { + /// Creates a fragment for [painter] painted at [origin], whose text begins at + /// [textStart] in the block's rendered text. + SelectableFragment(this.painter, this.origin, this.textStart); + + /// The text painter that owns this run's glyphs. + final TextPainter painter; + + /// Block-local top-left where [painter] is painted. + final Offset origin; + + /// Index of this run's first character in the block's rendered text. + final int textStart; + + /// Length of this run's text. + int get length => painter.plainText.length; + + /// One-past-the-last index of this run's text in the block's rendered text. + int get textEnd => textStart + length; +} + +/// Provides [SelectableBlockPainter] for a block whose text is spread across +/// several [TextPainter]s painted at different origins (lists, tables). +/// +/// [fragments] must be listed in the same order their text appears in +/// [markdownBlockRenderedText], with each fragment's `textStart` matching that +/// linearization (the gaps between fragments are the `\n`/`\t` separators, which +/// have no glyphs of their own). [renderedText] must equal that linearization. +mixin MultiPainterSelectable implements SelectableBlockPainter { + /// The selectable runs of this block, in rendered-text order. Rebuilt on each + /// [BlockPainter.layout]. + List get fragments; + + @override + int offsetForLocalPosition(Offset local) { + final fragment = _nearestFragment(local); + if (fragment == null) return 0; + final inner = + fragment.painter.getPositionForOffset(local - fragment.origin).offset; + return fragment.textStart + inner.clamp(0, fragment.length); + } + + @override + List boxesForRange(int start, int end) { + final out = []; + for (final fragment in fragments) { + final localStart = start.clamp(fragment.textStart, fragment.textEnd) - + fragment.textStart; + final localEnd = + end.clamp(fragment.textStart, fragment.textEnd) - fragment.textStart; + if (localEnd <= localStart) continue; + final boxes = fragment.painter.getBoxesForSelection( + TextSelection(baseOffset: localStart, extentOffset: localEnd)); + for (final box in boxes) { + out.add(box.toRect().shift(fragment.origin)); + } + } + return out; + } + + @override + TextRange wordBoundaryForLocal(Offset local) { + final fragment = _nearestFragment(local); + if (fragment == null) return const TextRange(start: 0, end: 0); + final inner = + fragment.painter.getPositionForOffset(local - fragment.origin).offset; + final wb = fragment.painter.getWordBoundary(TextPosition(offset: inner)); + // Keep the word within its own fragment (never cross a cell/item boundary). + return TextRange( + start: fragment.textStart + wb.start.clamp(0, fragment.length), + end: fragment.textStart + wb.end.clamp(0, fragment.length), + ); + } + + @override + bool isLinkAtLocal(Offset local) { + for (final fragment in fragments) { + if ((fragment.origin & fragment.painter.size).contains(local)) { + return _spanHasRecognizerAt(fragment.painter, local - fragment.origin); + } + } + return false; + } + + SelectableFragment? _nearestFragment(Offset local) { + SelectableFragment? best; + var bestDistance = double.infinity; + for (final fragment in fragments) { + final distance = + _distanceToRect(local, fragment.origin & fragment.painter.size); + if (distance < bestDistance) { + bestDistance = distance; + best = fragment; + if (distance == 0) break; + } + } + return best; + } +} + +/// Whether the span under a text-local [local] point in [painter] carries a tap +/// recognizer (i.e. an actionable link). False for points outside the text box. +bool _spanHasRecognizerAt(TextPainter painter, Offset local) { + final size = painter.size; + if (local.dx < 0 || + local.dy < 0 || + local.dx > size.width || + local.dy > size.height) { + return false; + } + final span = + painter.text?.getSpanForPosition(painter.getPositionForOffset(local)); + return span is TextSpan && span.recognizer != null; +} + +/// Shortest distance from [point] to [rect] (0 when the point is inside). +double _distanceToRect(Offset point, Rect rect) { + final dx = point.dx < rect.left + ? rect.left - point.dx + : (point.dx > rect.right ? point.dx - rect.right : 0.0); + final dy = point.dy < rect.top + ? rect.top - point.dy + : (point.dy > rect.bottom ? point.dy - rect.bottom : 0.0); + if (dx == 0) return dy; + if (dy == 0) return dx; + return math.sqrt(dx * dx + dy * dy); +} + +/// Mixin for block painters backed by a [TextPainter] that want to fire link +/// (or other) tap recognizers: [hitTestInlineSpanWithPointerEvent] resolves the +/// [InlineSpan] under a pointer so [BlockPainter.handleTapDown] / +/// [BlockPainter.handleTapUp] can match down and up on the same span. +mixin ParagraphGestureHandler { + /// The [InlineSpan] under [event] within [painter], or null if none. + @protected + InlineSpan? hitTestInlineSpanWithPointerEvent( + PointerEvent event, TextPainter painter) { + final pos = painter.getPositionForOffset(event.localPosition); + final span = painter.text?.getSpanForPosition(pos); + return span; + } +} diff --git a/lib/src/render/blocks/alert.dart b/lib/src/render/blocks/alert.dart new file mode 100644 index 0000000..e543755 --- /dev/null +++ b/lib/src/render/blocks/alert.dart @@ -0,0 +1,174 @@ +//ignore_for_file: unnecessary_import + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../nodes.dart'; +import '../../theme.dart'; +import '../block_painter.dart'; +import '../span_builder.dart'; + +/// A class for painting a GitHub-style alert (admonition) block in markdown. +class BlockPainter$Alert + with ParagraphGestureHandler, SelectableTextBlock + implements BlockPainter { + @override + TextPainter get selectionPainter => bodyPainter; + @override + Offset get selectionOrigin => _bodyOrigin; + + /// Creates an alert painter of kind [alert] with body [spans], styled by + /// [theme]. + BlockPainter$Alert({ + required this.alert, + required List spans, + required this.theme, + }) : _accent = theme.alertColorFor(alert), + titlePainter = TextPainter( + text: TextSpan( + text: alert.title, + style: theme.textStyle.copyWith( + color: theme.alertColorFor(alert), + fontWeight: FontWeight.bold, + ), + ), + textAlign: TextAlign.start, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ), + bodyPainter = TextPainter( + text: paragraphFromMarkdownSpans(spans: spans, theme: theme), + textAlign: TextAlign.start, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ); + + /// The kind of the alert being painted. + final MD$AlertType alert; + + /// The theme used to style the alert. + final MarkdownThemeData theme; + + final Color _accent; + + /// Painter for the alert title (e.g. "Note"). + final TextPainter titlePainter; + + /// Painter for the alert body content. + final TextPainter bodyPainter; + + /// Padding around the alert's content, inside its rounded background. + static const double padding = 10.0; + + /// Width of the accent bar on the left edge. + static const double barWidth = 4.0; + + /// Gap between the accent bar and the content. + static const double gap = 10.0; + + /// Vertical gap between the title and the body. + static const double titleGap = 4.0; + + /// Left offset where the title/body content begins. + double get _contentLeft => padding + barWidth + gap; + + @override + Size get size => _size; + Size _size = Size.zero; + + /// Whether the body has any content to paint. + bool _hasBody = false; + + /// Last span hit by the tap down event. + TextSpan? _lastSpan; + + Offset get _bodyOrigin => + Offset(_contentLeft, padding + titlePainter.height + titleGap); + + TextSpan? _spanForPosition(Offset localPosition) { + if (!_hasBody) return null; + final local = localPosition - _bodyOrigin; + if (local.dx < 0 || + local.dy < 0 || + local.dx > bodyPainter.width || + local.dy > bodyPainter.height) return null; + final position = bodyPainter.getPositionForOffset(local); + final span = bodyPainter.text?.getSpanForPosition(position); + return span is TextSpan ? span : null; + } + + @override + void handleTapDown(PointerDownEvent event) { + _lastSpan = _spanForPosition(event.localPosition); + } + + @override + void handleTapUp(PointerUpEvent event) { + if (_lastSpan == null) return; + final span = _spanForPosition(event.localPosition); + if (span != null && _lastSpan == span) { + if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) + onTap?.call(); + } + _lastSpan = null; + } + + @override + Size layout(double width) { + final available = math.max(0.0, width - _contentLeft - padding); + titlePainter.layout(minWidth: 0, maxWidth: available); + bodyPainter.layout(minWidth: 0, maxWidth: available); + _hasBody = bodyPainter.text?.toPlainText().isNotEmpty ?? false; + + final contentWidth = math.max( + titlePainter.width, + _hasBody ? bodyPainter.width : 0.0, + ); + final contentHeight = + titlePainter.height + (_hasBody ? titleGap + bodyPainter.height : 0.0); + return _size = Size( + _contentLeft + contentWidth + padding, + contentHeight + padding * 2, + ); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + if (size.width < _size.width) return; + + final rect = Rect.fromLTWH(0, offset, size.width, _size.height); + // Tinted background. + canvas.drawRRect( + RRect.fromRectAndRadius(rect, const Radius.circular(6.0)), + Paint() + ..color = _accent.withValues(alpha: 0.10) + ..style = PaintingStyle.fill, + ); + // Accent bar on the left. + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, offset, barWidth, _size.height), + const Radius.circular(barWidth / 2), + ), + Paint() + ..color = _accent + ..style = PaintingStyle.fill, + ); + + titlePainter.paint(canvas, Offset(_contentLeft, offset + padding)); + if (_hasBody) { + bodyPainter.paint(canvas, _bodyOrigin + Offset(0, offset)); + } + } + + @override + void dispose() { + titlePainter.dispose(); + bodyPainter.dispose(); + } +} diff --git a/lib/src/render/blocks/code.dart b/lib/src/render/blocks/code.dart new file mode 100644 index 0000000..d44733c --- /dev/null +++ b/lib/src/render/blocks/code.dart @@ -0,0 +1,119 @@ +//ignore_for_file: unnecessary_import + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../theme.dart'; +import '../block_painter.dart'; + +/// A class for painting a code block in markdown. +class BlockPainter$Code with SelectableTextBlock implements BlockPainter { + @override + TextPainter get selectionPainter => painter; + @override + Offset get selectionOrigin => const Offset(padding, padding); + + /// Creates a code-block painter for [text] in [language], styled by [theme]. + BlockPainter$Code({ + required String text, + required String? language, + required this.theme, + }) : _background = theme.highlighter?.backgroundFor(language) ?? + theme.surfaceColor ?? + const Color.fromARGB(255, 235, 235, 235), + painter = TextPainter( + text: _buildSpan(text, language, theme), + textAlign: TextAlign.start, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ); + + /// Builds the code span: plain monospace text, or, when the theme carries a + /// [MarkdownThemeData.highlighter], a tree of colored token spans whose + /// concatenated text still equals [text] (so selection stays aligned). + static TextSpan _buildSpan( + String text, + String? language, + MarkdownThemeData theme, + ) { + final baseStyle = theme.textStyle.copyWith( + fontFamily: 'monospace', + fontSize: theme.textStyle.fontSize ?? kDefaultFontSize, + ); + final highlighter = theme.highlighter; + if (highlighter == null) return TextSpan(text: text, style: baseStyle); + final effectiveBase = highlighter.baseStyleFor(language, baseStyle); + return TextSpan( + style: effectiveBase, + children: highlighter.highlight(text, language, effectiveBase), + ); + } + + /// Padding around the code text, inside its rounded background. + static const double padding = 8.0; + + /// The theme used to style the code block. + final MarkdownThemeData theme; + + /// Background color of the code block surface. + final Color _background; + + /// The text painter that owns the code's glyphs. + final TextPainter painter; + + @override + Size get size => _size; + Size _size = Size.zero; + + @override + void handleTapDown(PointerDownEvent _) {/* Do nothing */} + + @override + void handleTapUp(PointerUpEvent _) {/* Do nothing */} + + @override + Size layout(double width) { + if (width <= padding * 2) { + // If the width is less than or equal to padding, return zero size. + _size = Size.zero; + return _size; + } + painter.layout( + minWidth: 0, + maxWidth: width - padding * 2, + ); + return _size = Size( + painter.size.width + padding * 2, // Add padding to the width. + painter.size.height + padding * 2, // Add padding to the height. + ); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // If the width is less than required do not paint anything. + if (size.width < _size.width) return; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, offset, size.width, _size.height), + const Radius.circular(padding), + ), + Paint() + ..color = _background + ..isAntiAlias = false + ..style = PaintingStyle.fill, + ); + painter.paint( + canvas, + Offset(padding, offset + padding), + ); + } + + @override + void dispose() { + painter.dispose(); + } +} diff --git a/lib/src/render/blocks/divider.dart b/lib/src/render/blocks/divider.dart new file mode 100644 index 0000000..9bfb3c3 --- /dev/null +++ b/lib/src/render/blocks/divider.dart @@ -0,0 +1,60 @@ +//ignore_for_file: unnecessary_import + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../theme.dart'; +import '../block_painter.dart'; + +/// A class for painting a spacer block in markdown. +class BlockPainter$Divider implements BlockPainter { + /// Creates a thematic-break (horizontal rule) painter styled by [theme]. + BlockPainter$Divider({ + required this.theme, + }) : _paint = Paint() + ..color = theme.textStyle.color ?? const Color(0xFF000000) + ..isAntiAlias = false + ..strokeWidth = 1.0 + ..style = PaintingStyle.fill; + + final Paint _paint; + + /// The theme used to color and size the divider. + final MarkdownThemeData theme; + + @override + Size get size => _size; + Size _size = Size.zero; + + @override + void handleTapDown(PointerDownEvent _) {/* Do nothing */} + + @override + void handleTapUp(PointerUpEvent _) {/* Do nothing */} + + @override + Size layout(double width) { + final height = theme.textStyle.fontSize ?? kDefaultFontSize; + return _size = Size(0, height); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // Draw a horizontal line across the width of the canvas. + final center = offset + _size.height / 2; + canvas.drawLine( + Offset(0, center), + Offset(size.width, center), + _paint, + ); + } + + @override + void dispose() { + // Noting to dispose + } +} diff --git a/lib/src/render/blocks/heading.dart b/lib/src/render/blocks/heading.dart new file mode 100644 index 0000000..8315d10 --- /dev/null +++ b/lib/src/render/blocks/heading.dart @@ -0,0 +1,94 @@ +//ignore_for_file: unnecessary_import + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../nodes.dart'; +import '../../theme.dart'; +import '../block_painter.dart'; +import '../span_builder.dart'; + +/// A class for painting a heading block in markdown. +class BlockPainter$Heading + with ParagraphGestureHandler, SelectableTextBlock + implements BlockPainter { + @override + TextPainter get selectionPainter => painter; + + /// Creates a heading painter for [spans] at [level] (1-6), styled by [theme]. + BlockPainter$Heading({ + required int level, + required List spans, + required this.theme, + }) : painter = TextPainter( + text: paragraphFromMarkdownSpans( + spans: spans, + theme: theme, + textStyle: theme.headingStyleFor(level), + ), + textAlign: TextAlign.start, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ); + + /// The theme used to style the heading. + final MarkdownThemeData theme; + + /// The text painter that owns the heading's glyphs. + final TextPainter painter; + + @override + Size get size => _size; + Size _size = Size.zero; + + /// Last span hit by the tap down event. + TextSpan? _lastSpan; + + @override + void handleTapDown(PointerDownEvent event) { + _lastSpan = null; // Reset the span on tap down. + final span = hitTestInlineSpanWithPointerEvent(event, painter); + if (span case TextSpan textSpan) _lastSpan = textSpan; + } + + @override + void handleTapUp(PointerUpEvent event) { + if (_lastSpan == null) return; // No span was hit on tap down. + final span = hitTestInlineSpanWithPointerEvent(event, painter); + if (span != null && _lastSpan == span) { + // If the span is the same as the one hit on tap down, + // call the tap recognizer. + if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) + onTap?.call(); + } + _lastSpan = null; // Clear the span after handling the tap. + } + + @override + Size layout(double width) { + painter.layout( + minWidth: 0, + maxWidth: width, + ); + return _size = painter.size; + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // If the width is less than required do not paint anything. + if (size.width < _size.width) return; + painter.paint( + canvas, + Offset(0, offset), + ); + } + + @override + void dispose() { + painter.dispose(); + } +} diff --git a/lib/src/render/blocks/list.dart b/lib/src/render/blocks/list.dart new file mode 100644 index 0000000..fe538db --- /dev/null +++ b/lib/src/render/blocks/list.dart @@ -0,0 +1,203 @@ +//ignore_for_file: unnecessary_import + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../nodes.dart'; +import '../../theme.dart'; +import '../block_painter.dart'; +import '../span_builder.dart'; + +/// A helper class to store layout information for a single list item. +class _ListItemMetrics { + _ListItemMetrics({ + required this.bulletPainter, + required this.contentPainter, + required this.offset, + }); + + final TextPainter bulletPainter; + final TextPainter contentPainter; + final Offset offset; + + late final double height = + math.max(bulletPainter.height, contentPainter.height); + late final Size size = + Size(bulletPainter.width + contentPainter.width, height); + + void dispose() { + bulletPainter.dispose(); + contentPainter.dispose(); + } +} + +/// A class for painting a list block in markdown. +class BlockPainter$List + with ParagraphGestureHandler, MultiPainterSelectable + implements BlockPainter { + /// Creates a list painter for [items] (bulleted, ordered or task list), + /// styled by [theme]. + BlockPainter$List({ + required List items, + required this.theme, + }) : _items = items, + _painters = <_ListItemMetrics>[]; + + /// The theme used to style the list. + final MarkdownThemeData theme; + final List _items; + final List<_ListItemMetrics> _painters; + + @override + List get fragments => _fragments; + List _fragments = const []; + + @override + String get renderedText => _renderedText; + String _renderedText = ''; + + // Indentation for the entire list block. + static const double _baseIndent = 8.0; + + // Indentation for each level of nesting. + static const double _levelIndent = 16.0; + + @override + Size get size => _size; + Size _size = Size.zero; + + /// Last span hit by the tap down event. + InlineSpan? _lastSpan; + + InlineSpan? _getSpanForPosition(Offset localPosition) { + for (final metrics in _painters) { + final contentOffset = + metrics.offset + Offset(metrics.bulletPainter.width, 0); + final contentRect = contentOffset & metrics.contentPainter.size; + if (contentRect.contains(localPosition)) { + final painterPosition = localPosition - contentOffset; + final textPosition = + metrics.contentPainter.getPositionForOffset(painterPosition); + return metrics.contentPainter.text?.getSpanForPosition(textPosition); + } + } + return null; + } + + @override + void handleTapDown(PointerDownEvent event) { + _lastSpan = null; // Reset the span on tap down. + _lastSpan = _getSpanForPosition(event.localPosition); + } + + @override + void handleTapUp(PointerUpEvent event) { + if (_lastSpan == null) return; // No span was hit on tap down. + final newSpan = _getSpanForPosition(event.localPosition); + if (newSpan != null && _lastSpan == newSpan) { + if (newSpan + case TextSpan(recognizer: final TapGestureRecognizer recognizer)) { + recognizer.onTap?.call(); + } + } + + _lastSpan = null; // Clear the span after handling the tap. + } + + @override + Size layout(double width) { + for (final painter in _painters) { + painter.dispose(); + } + _painters.clear(); + + double currentHeight = 0; + double maxContentWidth = 0; + + void layoutItems(List items, int level) { + final indent = _baseIndent + level * _levelIndent; + for (final item in items) { + // Task-list items render a checkbox instead of a bullet/number. + final bulletText = switch (item.checked) { + true => 'โ˜‘', + false => 'โ˜', + null => switch (item.marker) { + '-' || '*' || '+' => 'โ€ข', + _ => item.marker, + }, + }; + final bulletPainter = TextPainter( + text: TextSpan(text: '$bulletText ', style: theme.textStyle), + textDirection: theme.textDirection, + textScaler: theme.textScaler, + )..layout(); + + final contentPainter = TextPainter( + text: paragraphFromMarkdownSpans(spans: item.spans, theme: theme), + textDirection: theme.textDirection, + textScaler: theme.textScaler, + )..layout(maxWidth: math.max(0, width - indent - bulletPainter.width)); + + final metrics = _ListItemMetrics( + bulletPainter: bulletPainter, + contentPainter: contentPainter, + offset: Offset(indent, currentHeight), + ); + _painters.add(metrics); + + currentHeight += metrics.height; + maxContentWidth = + math.max(maxContentWidth, indent + metrics.size.width); + + if (item.children.isNotEmpty) { + layoutItems(item.children, level + 1); + } + } + } + + layoutItems(_items, 0); + _rebuildFragments(); + return _size = Size(maxContentWidth, currentHeight); + } + + /// Rebuilds the selectable fragments from the laid-out item metrics. Items + /// (depth-first, matching `markdownBlockRenderedText`) are joined by `\n`. + void _rebuildFragments() { + final frags = []; + var textPos = 0; + for (final metrics in _painters) { + if (frags.isNotEmpty) textPos += 1; // the '\n' item separator + final origin = metrics.offset + Offset(metrics.bulletPainter.width, 0); + frags.add(SelectableFragment(metrics.contentPainter, origin, textPos)); + textPos += metrics.contentPainter.plainText.length; + } + _fragments = frags; + _renderedText = frags.map((f) => f.painter.plainText).join('\n'); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + for (final metrics in _painters) { + final bulletOffset = metrics.offset + Offset(0, offset); + metrics.bulletPainter.paint(canvas, bulletOffset); + + final contentOffset = + bulletOffset + Offset(metrics.bulletPainter.width, 0); + metrics.contentPainter.paint(canvas, contentOffset); + } + } + + @override + void dispose() { + for (final metrics in _painters) { + metrics.dispose(); + } + _painters.clear(); + _fragments = const []; + } +} diff --git a/lib/src/render/blocks/paragraph.dart b/lib/src/render/blocks/paragraph.dart new file mode 100644 index 0000000..aac47e0 --- /dev/null +++ b/lib/src/render/blocks/paragraph.dart @@ -0,0 +1,92 @@ +//ignore_for_file: unnecessary_import + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../nodes.dart'; +import '../../theme.dart'; +import '../block_painter.dart'; +import '../span_builder.dart'; + +/// A class for painting a paragraph block in markdown. +class BlockPainter$Paragraph + with ParagraphGestureHandler, SelectableTextBlock + implements BlockPainter { + @override + TextPainter get selectionPainter => painter; + + /// Creates a paragraph painter for [spans], styled by [theme]. + BlockPainter$Paragraph({ + required List spans, + required this.theme, + }) : painter = TextPainter( + text: paragraphFromMarkdownSpans( + spans: spans, + theme: theme, + ), + textAlign: TextAlign.start, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ); + + /// The theme used to style the paragraph. + final MarkdownThemeData theme; + + /// The text painter that owns the paragraph's glyphs. + final TextPainter painter; + + @override + Size get size => _size; + Size _size = Size.zero; + + /// Last span hit by the tap down event. + TextSpan? _lastSpan; + + @override + void handleTapDown(PointerDownEvent event) { + _lastSpan = null; // Reset the span on tap down. + final span = hitTestInlineSpanWithPointerEvent(event, painter); + if (span case TextSpan textSpan) _lastSpan = textSpan; + } + + @override + void handleTapUp(PointerUpEvent event) { + if (_lastSpan == null) return; // No span was hit on tap down. + final span = hitTestInlineSpanWithPointerEvent(event, painter); + if (span != null && _lastSpan == span) { + // If the span is the same as the one hit on tap down, + // call the tap recognizer. + if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) + onTap?.call(); + } + _lastSpan = null; // Clear the span after handling the tap. + } + + @override + Size layout(double width) { + painter.layout( + minWidth: 0, + maxWidth: width, + ); + return _size = painter.size; + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // If the width is less than required do not paint anything. + if (size.width < _size.width) return; + painter.paint( + canvas, + Offset(0, offset), + ); + } + + @override + void dispose() { + painter.dispose(); + } +} diff --git a/lib/src/render/blocks/quote.dart b/lib/src/render/blocks/quote.dart new file mode 100644 index 0000000..5a8fb3c --- /dev/null +++ b/lib/src/render/blocks/quote.dart @@ -0,0 +1,135 @@ +//ignore_for_file: unnecessary_import + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../nodes.dart'; +import '../../theme.dart'; +import '../block_painter.dart'; +import '../span_builder.dart'; + +/// A class for painting a quote block in markdown. +class BlockPainter$Quote + with ParagraphGestureHandler, SelectableTextBlock + implements BlockPainter { + @override + TextPainter get selectionPainter => painter; + @override + Offset get selectionOrigin => Offset(lineIndent + indent * lineIndent, 0); + + /// Creates a quote painter for [spans] nested [indent] levels deep, styled + /// by [theme]. + BlockPainter$Quote({ + required List spans, + required this.indent, + required this.theme, + }) : painter = TextPainter( + text: paragraphFromMarkdownSpans( + spans: spans, + theme: theme, + textStyle: theme.quoteStyle ?? theme.textStyle, + ), + textAlign: TextAlign.start, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ), + linePaint = Paint() + ..color = theme.dividerColor ?? + const Color(0x7F7F7F7F) // Gray color for the line. + ..isAntiAlias = false + ..strokeWidth = 4.0 + ..style = PaintingStyle.fill; + + /// The theme used to style the quote. + final MarkdownThemeData theme; + + /// The text painter that owns the quote's glyphs. + final TextPainter painter; + + /// Nesting depth of the quote (0 for a top-level quote). + final int indent; + + /// Horizontal space taken by each nesting level's accent bar. + static const double lineIndent = 10.0; + + /// Paint used for the vertical accent bar(s) on the left. + final Paint linePaint; + + @override + Size get size => _size; + Size _size = Size.zero; + + /// Last span hit by the tap down event. + TextSpan? _lastSpan; + + @override + void handleTapDown(PointerDownEvent event) { + _lastSpan = null; // Reset the span on tap down. + final span = hitTestInlineSpanWithPointerEvent(event, painter); + if (span case TextSpan textSpan) _lastSpan = textSpan; + } + + @override + void handleTapUp(PointerUpEvent event) { + if (_lastSpan == null) return; // No span was hit on tap down. + final span = hitTestInlineSpanWithPointerEvent(event, painter); + if (span != null && _lastSpan == span) { + // If the span is the same as the one hit on tap down, + // call the tap recognizer. + if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) + onTap?.call(); + } + _lastSpan = null; // Clear the span after handling the tap. + } + + @override + Size layout(double width) { + // Adjust width for indentation. + painter.layout( + minWidth: 0, + maxWidth: math.max(width - lineIndent - indent * lineIndent, 0), + ); + return _size = Size( + painter.size.width + lineIndent + indent * lineIndent, + painter.size.height, + ); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // If the width is less than required do not paint anything. + if (size.width < _size.width) return; + + // --- Draw vertical lines --- // + for (var i = 1; i <= indent; i++) + canvas.drawLine( + Offset( + i * lineIndent, + offset, + ), + Offset( + i * lineIndent, + offset + _size.height, + ), + linePaint, + ); + + painter.paint( + canvas, + Offset( + lineIndent + indent * lineIndent, + offset, + ), + ); + } + + @override + void dispose() { + painter.dispose(); + } +} diff --git a/lib/src/render/blocks/spacer.dart b/lib/src/render/blocks/spacer.dart new file mode 100644 index 0000000..68be44a --- /dev/null +++ b/lib/src/render/blocks/spacer.dart @@ -0,0 +1,56 @@ +//ignore_for_file: unnecessary_import + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../theme.dart'; +import '../block_painter.dart'; + +/// A class for painting a spacer block in markdown. +class BlockPainter$Spacer implements BlockPainter { + /// Creates a spacer painter [count] blank lines tall, sized by [theme]. + BlockPainter$Spacer({ + required this.count, + required this.theme, + }); + + /// Number of blank lines this spacer occupies. + final int count; + + /// The theme whose text size determines the spacer's height. + final MarkdownThemeData theme; + + @override + Size get size => _size; + Size _size = Size.zero; + + @override + void handleTapDown(PointerDownEvent _) {/* Do nothing */} + + @override + void handleTapUp(PointerUpEvent _) {/* Do nothing */} + + @override + Size layout(double width) { + final height = theme.textStyle.fontSize ?? kDefaultFontSize; + return _size = Size(0, height * count); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // Do not paint anything + /* canvas.drawRect( + Rect.fromLTWH(0, offset, size.width, _size.height), + Paint()..color = theme.textStyle.color ?? const Color(0x00000000), + ); */ + } + + @override + void dispose() { + // Noting to dispose + } +} diff --git a/lib/src/render/blocks/table.dart b/lib/src/render/blocks/table.dart new file mode 100644 index 0000000..8e8b55c --- /dev/null +++ b/lib/src/render/blocks/table.dart @@ -0,0 +1,420 @@ +//ignore_for_file: unnecessary_import + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../../nodes.dart'; +import '../../theme.dart'; +import '../block_painter.dart'; +import '../span_builder.dart'; + +/// A class for painting a table block in markdown. +class BlockPainter$Table + with ParagraphGestureHandler, MultiPainterSelectable + implements BlockPainter { + /// Creates a table painter for the [header] row and data [rows], with + /// per-column [alignments], styled by [theme]. + BlockPainter$Table({ + required this.header, + required this.rows, + required this.theme, + this.alignments = const [], + }) : columns = header.cells.length, + _columnWidths = List.filled(header.cells.length, 0.0), + _rowHeights = List.filled(rows.length + 1, 0.0), + _borderPaint = Paint() + ..color = theme.dividerColor ?? const Color(0x1F000000) + ..style = PaintingStyle.stroke + ..isAntiAlias = false + ..strokeWidth = 1.0, + _rowBackgroundPaint = Paint() + ..style = PaintingStyle.fill + ..isAntiAlias = false + ..color = + theme.surfaceColor ?? const Color.fromARGB(255, 235, 235, 235); + + /// Padding for table cells. + static const double padding = 8.0; + + /// The theme for the markdown table. + final MarkdownThemeData theme; + + /// The number of columns in the table. + final int columns; + + /// The per-column alignment derived from the delimiter row. + final List alignments; + + /// Resolves the alignment for column [c], defaulting to + /// [MD$TableColumnAlign.none] when unspecified. + MD$TableColumnAlign _columnAlign(int c) => c >= 0 && c < alignments.length + ? alignments[c] + : MD$TableColumnAlign.none; + + /// The horizontal offset of a cell's text within its column, honoring the + /// column alignment (falling back to centered headers / left-aligned data). + double _cellHorizontalPadding(int r, int c, double painterWidth) => + switch (_columnAlign(c)) { + MD$TableColumnAlign.left => padding, + MD$TableColumnAlign.center => (_columnWidths[c] - painterWidth) / 2, + MD$TableColumnAlign.right => _columnWidths[c] - painterWidth - padding, + MD$TableColumnAlign.none => + (r == 0) ? (_columnWidths[c] - painterWidth) / 2 : padding, + }; + + final List _columnWidths; + final List _rowHeights; + final Paint _borderPaint; + final Paint _rowBackgroundPaint; + + Float32List? _borderPoints; + + /// The header row of the table. + final MD$TableRow header; + + /// The rows of the table. + final List rows; + + @override + Size get size => _size; + Size _size = Size.zero; + + List> _cellPainters = const []; + + @override + List get fragments => _fragments; + List _fragments = const []; + + @override + String get renderedText => _renderedText; + String _renderedText = ''; + + /// Last span hit by the tap down event. + TextSpan? _lastSpan; + + @override + void handleTapDown(PointerDownEvent event) { + _lastSpan = null; // Reset the span on tap down. + final span = _getSpanForOffset(event.localPosition); + if (span != null) { + _lastSpan = span; + } + } + + @override + void handleTapUp(PointerUpEvent event) { + if (_lastSpan == null) return; // No span was hit on tap down. + final span = _getSpanForOffset(event.localPosition); + if (span != null && _lastSpan == span) { + // If the span is the same as the one hit on tap down, + // call the tap recognizer. + if (span case TextSpan(recognizer: TapGestureRecognizer(:var onTap))) + onTap?.call(); + } + _lastSpan = null; // Clear the span after handling the tap. + } + + TextSpan? _getSpanForOffset(Offset position) { + double currentY = 0.0; + + for (int r = 0; r < _cellPainters.length; r++) { + final rowHeight = _rowHeights[r]; + double currentX = 0.0; + + if (position.dy >= currentY && position.dy < currentY + rowHeight) { + // In this row. + for (int c = 0; c < _cellPainters[r].length; c++) { + final painter = _cellPainters[r][c]; + if (painter.text == null) { + currentX += _columnWidths[c]; + continue; + } + final columnWidth = _columnWidths[c]; + + if (position.dx >= currentX && position.dx < currentX + columnWidth) { + // In this cell. + final verticalPadding = (rowHeight - painter.height) / 2; + final horizontalPadding = + _cellHorizontalPadding(r, c, painter.width); + + final painterOffset = Offset( + currentX + horizontalPadding, currentY + verticalPadding); + final localPosition = position - painterOffset; + + // Check if inside the actual painted text area. + if (localPosition.dx < 0 || + localPosition.dx > painter.width || + localPosition.dy < 0 || + localPosition.dy > painter.height) { + currentX += columnWidth; + continue; + } + + final textPosition = painter.getPositionForOffset(localPosition); + final span = painter.text!.getSpanForPosition(textPosition); + if (span is TextSpan) { + return span; + } + return null; // Found cell, but no span. + } + currentX += columnWidth; + } + } + currentY += rowHeight; + } + return null; + } + + @override + Size layout(double width) { + if (columns < 1) return _size = Size.zero; + + // Dispose old painters + for (final row in _cellPainters) { + for (final painter in row) { + painter.dispose(); + } + } + + final allRows = [header, ...rows]; + final naturalWidths = List.filled(columns, 0.0); + final minWidths = List.filled(columns, 0.0); + + // Create painters for each row and column and calculate natural widths + _cellPainters = List.generate(allRows.length, (r) { + final row = allRows[r]; + return List.generate(columns, (c) { + if (c >= row.cells.length) { + return TextPainter(textDirection: theme.textDirection); + } + final cell = row.cells[c]; + final style = (r == 0) + ? theme.textStyle.copyWith(fontWeight: FontWeight.bold) + : null; + final textPainter = TextPainter( + text: paragraphFromMarkdownSpans( + spans: cell, theme: theme, textStyle: style), + textAlign: switch (_columnAlign(c)) { + MD$TableColumnAlign.left => TextAlign.left, + MD$TableColumnAlign.center => TextAlign.center, + MD$TableColumnAlign.right => TextAlign.right, + MD$TableColumnAlign.none => + (r == 0) ? TextAlign.center : TextAlign.start, + }, + textDirection: theme.textDirection, + textScaler: theme.textScaler, + ); + + // Calculate natural width + textPainter.layout(maxWidth: double.infinity); + naturalWidths[c] = + math.max(naturalWidths[c], textPainter.width + padding * 2); + + // Calculate min width (longest word) + final cellText = cell.map((s) => s.text).join(); + final words = cellText.split(RegExp(r'\s+')); + if (words.isNotEmpty) { + final longestWord = + words.reduce((a, b) => a.length > b.length ? a : b); + final wordPainter = TextPainter( + text: TextSpan(text: longestWord, style: style), + textDirection: theme.textDirection, + )..layout(); + minWidths[c] = + math.max(minWidths[c], wordPainter.width + padding * 2); + wordPainter.dispose(); + } + + return textPainter; + }); + }); + + _columnWidths.setAll(0, _distributeWidths(naturalWidths, minWidths, width)); + + final totalWidth = _columnWidths.reduce((a, b) => a + b); + + // Layout painters with final widths and calculate row heights + + double totalHeight = 0.0; + for (int r = 0; r < allRows.length; r++) { + double rowHeight = 0.0; + for (int c = 0; c < columns; c++) { + final painter = _cellPainters[r][c]; + if (painter.text == null) continue; + painter.layout(maxWidth: math.max(0.0, _columnWidths[c] - padding * 2)); + rowHeight = math.max( + rowHeight, + painter.height, + ); + } + + _rowHeights[r] = rowHeight + padding * 2; + totalHeight += _rowHeights[r]; + } + + // Cache border points + final points = Float32List(((allRows.length - 1) + (columns - 1)) * 4); + var pointIndex = 0; + // Horizontal lines + double lineY = 0; + for (int r = 0; r < allRows.length - 1; r++) { + lineY += _rowHeights[r]; + points[pointIndex++] = 0; + points[pointIndex++] = lineY; + points[pointIndex++] = totalWidth; + points[pointIndex++] = lineY; + } + // Vertical lines + double lineX = 0; + for (int c = 0; c < columns - 1; c++) { + lineX += _columnWidths[c]; + points[pointIndex++] = lineX; + points[pointIndex++] = 0; + points[pointIndex++] = lineX; + points[pointIndex++] = totalHeight; + } + _borderPoints = points; + + _rebuildFragments(allRows); + + return _size = Size(totalWidth, totalHeight); + } + + /// Rebuilds the selectable fragments (row-major: cells joined by `\t`, rows by + /// `\n`) so they line up with `markdownBlockRenderedText`. Only the cells that + /// exist in the source row contribute text, matching the painted cells. + void _rebuildFragments(List allRows) { + final frags = []; + final text = StringBuffer(); + var rowTop = 0.0; + for (var r = 0; r < allRows.length; r++) { + if (r > 0) text.write('\n'); + final cells = allRows[r].cells; + var colLeft = 0.0; + for (var c = 0; c < columns; c++) { + if (c < cells.length) { + if (c > 0) text.write('\t'); + final painter = _cellPainters[r][c]; + final verticalPadding = (_rowHeights[r] - painter.height) / 2; + final horizontalPadding = _cellHorizontalPadding(r, c, painter.width); + final origin = + Offset(colLeft + horizontalPadding, rowTop + verticalPadding); + frags.add(SelectableFragment(painter, origin, text.length)); + text.write(painter.plainText); + } + colLeft += _columnWidths[c]; + } + rowTop += _rowHeights[r]; + } + _fragments = frags; + _renderedText = text.toString(); + } + + @override + void paint(Canvas canvas, Size size, double offset) { + // If the width is less than required do not paint anything. + if (columns < 1) return; + + double currentY = offset; + + for (int r = 0; r < _cellPainters.length; r++) { + final rowHeight = _rowHeights[r]; + double currentX = 0; + + // Draw background for even data rows. + if (r % 2 == 0 && r != 0) { + canvas.drawRect( + Rect.fromLTWH(0, currentY, _size.width, rowHeight), + _rowBackgroundPaint, + ); + } + + for (int c = 0; c < columns; c++) { + final painter = _cellPainters[r][c]; + if (painter.text == null) { + currentX += _cellPainters[r].length > c ? _columnWidths[c] : 0; + continue; + } + + final verticalPadding = (rowHeight - painter.height) / 2; + final horizontalPadding = _cellHorizontalPadding(r, c, painter.width); + + painter.paint( + canvas, + Offset( + currentX + horizontalPadding, + currentY + verticalPadding, + ), + ); + currentX += _columnWidths[c]; + } + currentY += rowHeight; + } + + // Draw inner borders + if (_borderPoints != null) { + canvas.save(); + canvas.translate(0, offset); + canvas.drawRawPoints(PointMode.lines, _borderPoints!, _borderPaint); + canvas.restore(); + } + + // Draw outer borders + canvas.drawRect( + Rect.fromLTRB( + 0, + offset, + _size.width, + offset + _size.height, + ), + _borderPaint, + ); + } + + @override + void dispose() { + for (final row in _cellPainters) { + for (final painter in row) { + painter.dispose(); + } + } + _cellPainters = const []; + _fragments = const []; + } + + /// Helper function to distribute widths among columns, respecting minimums. + /// If total minimum width exceeds availableWidth, + /// it returns the minimum widths as-is, + /// implying that the content will overflow and require scrolling. + List _distributeWidths( + List natural, List min, double availableWidth) { + final totalNatural = natural.reduce((a, b) => a + b); + final totalMin = min.reduce((a, b) => a + b); + + if (totalNatural <= availableWidth) { + return natural; + } + + if (totalMin <= availableWidth) { + final remainingSpace = availableWidth - totalMin; + final extraSpacePerColumn = [ + for (var i = 0; i < natural.length; i++) natural[i] - min[i] + ]; + final totalExtraSpace = extraSpacePerColumn.reduce((a, b) => a + b); + + if (totalExtraSpace <= 0.001) return min; + + return [ + for (var i = 0; i < natural.length; i++) + min[i] + remainingSpace * (extraSpacePerColumn[i] / totalExtraSpace) + ]; + } + return min; + } +} diff --git a/lib/src/render/markdown_painter.dart b/lib/src/render/markdown_painter.dart new file mode 100644 index 0000000..266aa15 --- /dev/null +++ b/lib/src/render/markdown_painter.dart @@ -0,0 +1,426 @@ +//ignore_for_file: unnecessary_import + +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:meta/meta.dart' as meta show internal; + +import '../markdown.dart'; +import '../nodes.dart'; +import '../theme.dart'; +import 'block_painter.dart'; +import 'blocks/alert.dart'; +import 'blocks/code.dart'; +import 'blocks/divider.dart'; +import 'blocks/heading.dart'; +import 'blocks/list.dart'; +import 'blocks/paragraph.dart'; +import 'blocks/quote.dart'; +import 'blocks/spacer.dart'; +import 'blocks/table.dart'; + +/// A painter for rendering markdown content via blocks and spans. +@meta.internal +class MarkdownPainter { + /// Creates a [MarkdownPainter] instance. + MarkdownPainter({ + required Markdown markdown, + required MarkdownThemeData theme, + }) : _markdown = markdown, + _theme = theme, + _isEmpty = markdown.isEmpty, + _size = Size.zero { + _rebuild(); + } + + /// Is the markdown entity empty? + bool get isEmpty => _isEmpty; + bool _isEmpty; + + /// Current markdown entity to render. + Markdown _markdown; + + /// Current theme for the markdown widget. + MarkdownThemeData _theme; + + /// The size of the painted markdown content. + Size get size => _size; + Size _size; + + /// Indicates if the layout needs to be recalculated. + bool _needsLayout = true; + + Float32List _blockOffsets = Float32List(0); + List _blockPainters = const []; + + /// Source `Markdown.blocks` index for each painter (differs from the painter + /// index whenever a `blockFilter` drops blocks). + List _sourceIndices = const []; + + static BlockPainter _defaultBlockBuilder( + MD$Block block, + MarkdownThemeData theme, + ) => + block.map( + paragraph: (p) => BlockPainter$Paragraph( + spans: p.spans, + theme: theme, + ), + heading: (h) => BlockPainter$Heading( + level: h.level, + spans: h.spans, + theme: theme, + ), + quote: (q) => BlockPainter$Quote( + spans: q.spans, + indent: q.indent, + theme: theme, + ), + code: (c) => BlockPainter$Code( + language: c.language, + text: c.text, + theme: theme, + ), + list: (l) => BlockPainter$List( + items: l.items, + theme: theme, + ), + divider: (d) => BlockPainter$Divider( + theme: theme, + ), + table: (t) => BlockPainter$Table( + header: t.header, + rows: t.rows, + alignments: t.alignments, + theme: theme, + ), + alert: (a) => BlockPainter$Alert( + alert: a.alert, + spans: a.spans, + theme: theme, + ), + spacer: (s) => BlockPainter$Spacer( + count: s.count, + theme: theme, + ), + ); + + /// Rebuilds the block painters from the markdown blocks. + /// This method is called whenever the markdown or theme changes. + void _rebuild() { + _needsLayout = true; // Mark that layout needs to be recalculated. + _size = Size.zero; // Reset size before rebuilding. + final filter = _theme.blockFilter; + final builder = _theme.builder ?? _defaultBlockBuilder; + final blocks = _markdown.blocks; + final painters = []; + final sources = []; + for (var i = 0; i < blocks.length; i++) { + final block = blocks[i]; + if (filter != null && !filter(block)) continue; + painters + .add(builder(block, _theme) ?? _defaultBlockBuilder(block, _theme)); + sources.add(i); + } + _blockPainters = painters; + _sourceIndices = sources; + _blockOffsets = Float32List(_blockPainters.length); + } + + /// Binary-searches the painter index whose vertical band contains [dy]. + int _blockIndexForDy(double dy) { + var min = 0; + var max = _blockOffsets.length; + var idx = 0; + while (min < max) { + final mid = min + ((max - min) >> 1); + final offset = _blockOffsets[mid]; + if (offset > dy) { + max = mid; + } else { + idx = mid; + if (offset == dy) break; + min = mid + 1; + } + } + return idx; + } + + /// Maps a content-local [local] offset to `(sourceBlockIndex, offset)`, or + /// null if the hit block does not support selection. + (int, int)? positionForLocal(Offset local) { + if (_blockPainters.isEmpty) return null; + final idx = _blockIndexForDy(local.dy); + final painter = _blockPainters[idx]; + if (painter is! SelectableBlockPainter) return null; + final blockLocal = Offset(local.dx, local.dy - _blockOffsets[idx]); + final len = painter.renderedText.length; + final offset = painter.offsetForLocalPosition(blockLocal).clamp(0, len); + return (_sourceIndices[idx], offset); + } + + /// Maps a content-local [local] point to the word range at it, as + /// `(sourceBlockIndex, TextRange)`, or null if the hit block is not + /// selectable. Uses the platform word segmentation of the underlying painter. + (int, TextRange)? wordBoundaryForLocal(Offset local) { + if (_blockPainters.isEmpty) return null; + final idx = _blockIndexForDy(local.dy); + final painter = _blockPainters[idx]; + if (painter is! SelectableBlockPainter) return null; + final blockLocal = Offset(local.dx, local.dy - _blockOffsets[idx]); + final range = painter.wordBoundaryForLocal(blockLocal); + final len = painter.renderedText.length; + return ( + _sourceIndices[idx], + TextRange( + start: range.start.clamp(0, len), + end: range.end.clamp(0, len), + ), + ); + } + + /// Whether an actionable link sits under a content-local [local] point. + bool isLinkAtLocal(Offset local) { + if (_blockPainters.isEmpty) return false; + final idx = _blockIndexForDy(local.dy); + final painter = _blockPainters[idx]; + if (painter is! SelectableBlockPainter) return false; + final blockLocal = Offset(local.dx, local.dy - _blockOffsets[idx]); + return painter.isLinkAtLocal(blockLocal); + } + + /// Paints the selection highlight of every selectable block, using [rangeOf] + /// to look up the selected rendered range for a source block index. + void paintHighlight( + Canvas canvas, + TextRange? Function(int sourceIndex) rangeOf, + Paint paint, + ) { + for (var i = 0; i < _blockPainters.length; i++) { + final painter = _blockPainters[i]; + if (painter is! SelectableBlockPainter) continue; + final range = rangeOf(_sourceIndices[i]); + if (range == null || range.start >= range.end) continue; + final top = _blockOffsets[i]; + for (final rect in painter.boxesForRange(range.start, range.end)) { + canvas.drawRect(rect.shift(Offset(0, top)), paint); + } + } + } + + /// Content-local rectangles covering the selection described by [rangeOf], in + /// reading order. Same geometry [paintHighlight] draws, collected instead of + /// painted โ€” used to position selection handles, the magnifier and toolbar. + List selectionBoxes(TextRange? Function(int sourceIndex) rangeOf) { + final out = []; + for (var i = 0; i < _blockPainters.length; i++) { + final painter = _blockPainters[i]; + if (painter is! SelectableBlockPainter) continue; + final range = rangeOf(_sourceIndices[i]); + if (range == null || range.start >= range.end) continue; + final top = _blockOffsets[i]; + for (final rect in painter.boxesForRange(range.start, range.end)) { + out.add(rect.shift(Offset(0, top))); + } + } + return out; + } + + /// Update the painter with new values. + /// If the values are the same, + /// no update is required and the method returns false. + bool update({ + required Markdown markdown, + required MarkdownThemeData theme, + }) { + if (identical(_markdown, markdown) && identical(_theme, theme)) + return false; + _lastSize = null; + _lastPicture = null; + _markdown = markdown; + _theme = theme; + _isEmpty = markdown.isEmpty; + _rebuild(); + return true; // Indicate that the painter was updated. + } + + /// Invalidate cached layouts when system fonts change. + /// This forces TextPainters to recreate their layouts with new fonts. + void invalidateLayout() { + _needsLayout = true; + _lastSize = null; + _lastPicture = null; + // Dispose and rebuild all block painters to recreate TextPainters + // with the new system fonts + for (final painter in _blockPainters) { + painter.dispose(); + } + _rebuild(); + } + + /// Layouts the markdown content with the given width. + Size layout({required double maxWidth}) { + if (_isEmpty) { + _size = Size.zero; + _needsLayout = false; // No need to layout if the markdown is empty. + return _size; // If the markdown is empty, return zero size. + } + var width = .0, height = .0; + final blocks = _blockPainters; + if (_blockOffsets.length != blocks.length) { + // Resize the block sizes array + // if it does not match the number of painters. + _blockOffsets = Float32List(blocks.length); + } + final offsets = _blockOffsets; + for (var i = 0; i < blocks.length; i++) { + offsets[i] = height; + final block = blocks[i]; + final size = block.layout(maxWidth); + width = math.max(width, size.width); + height += size.height; + } + _needsLayout = false; // No need to layout if the markdown is empty. + return _size = Size(width, height); + } + + /// Routes a tap-down / tap-up to the block under the pointer, re-basing the + /// event's position into that block's local space (only taps are handled; the + /// block painters use it to fire link recognizers). + void handleEvent(PointerEvent event) { + if (_blockPainters.isEmpty) return; + if (event is! PointerDownEvent && event is! PointerUpEvent) return; + + final pos = event.localPosition; + final idx = _blockIndexForDy(pos.dy); + final blockLocal = Offset(pos.dx, pos.dy - _blockOffsets[idx]); + switch (event) { + case final PointerDownEvent down: + _blockPainters[idx].handleTapDown(_rebased(down, blockLocal)); + case final PointerUpEvent up: + _blockPainters[idx].handleTapUp(_rebased(up, blockLocal)); + } + } + + /// Copies [event] with its position moved to [localPosition] (block-local), + /// preserving every other pointer field. `PointerEvent` has no `copyWith`, so + /// the passthrough is spelled out; the return type follows the input type. + static T _rebased(T event, Offset localPosition) { + final rebased = switch (event) { + PointerUpEvent() => PointerUpEvent( + position: localPosition, + viewId: event.viewId, + timeStamp: event.timeStamp, + pointer: event.pointer, + kind: event.kind, + device: event.device, + buttons: event.buttons, + obscured: event.obscured, + pressure: event.pressure, + pressureMin: event.pressureMin, + pressureMax: event.pressureMax, + distanceMax: event.distanceMax, + size: event.size, + radiusMajor: event.radiusMajor, + radiusMinor: event.radiusMinor, + radiusMin: event.radiusMin, + radiusMax: event.radiusMax, + orientation: event.orientation, + tilt: event.tilt, + embedderId: event.embedderId, + ), + _ => PointerDownEvent( + position: localPosition, + viewId: event.viewId, + timeStamp: event.timeStamp, + pointer: event.pointer, + kind: event.kind, + device: event.device, + buttons: event.buttons, + obscured: event.obscured, + pressure: event.pressure, + pressureMin: event.pressureMin, + pressureMax: event.pressureMax, + distanceMax: event.distanceMax, + size: event.size, + radiusMajor: event.radiusMajor, + radiusMinor: event.radiusMinor, + radiusMin: event.radiusMin, + radiusMax: event.radiusMax, + orientation: event.orientation, + tilt: event.tilt, + embedderId: event.embedderId, + ), + }; + return rebased as T; + } + + /// The last size and picture used for painting. + /// This is used to avoid unnecessary recreation of the canvas picture. + /// If the size is the same as the last painted size, + Size? _lastSize; + + /// The last picture used for painting, + /// to avoid unnecessary recreation of the canvas picture. + /// If the size is the same as the last painted size, + /// we can reuse the last picture. + Picture? _lastPicture; + + /// The markdown content to paint. + void paint(Canvas canvas, Size size) { + assert( + !_needsLayout, + 'MarkdownPainter.paint() called without layout.', + ); + assert( + size.isFinite, + 'MarkdownPainter.paint() called with non-finite size: $size', + ); + + // Do not paint if the markdown is empty, + // or if the size is empty or infinite. + if (_isEmpty || size.isEmpty || size.isInfinite) return; + + if (_lastSize == size && _lastPicture != null) { + // If the size is the same as the last painted size, + // we can reuse the last picture. + canvas.drawPicture(_lastPicture!); + return; + } + + final recorder = PictureRecorder(); + final $canvas = Canvas(recorder); + + // Paint each block painter on the canvas. + var overflow = _size.height > size.height; + var offset = .0; + for (var painter in _blockPainters) { + if (overflow && offset > size.height) { + // If the painter's height exceeds the available height, + // we stop painting further blocks. + break; + } + painter.paint($canvas, size, offset); + offset += painter.size.height; // Update the offset for the next block. + } + + final picture = recorder.endRecording(); + canvas.drawPicture(picture); + _lastSize = size; + _lastPicture = picture; + } + + void dispose() { + _lastPicture?.dispose(); + _lastPicture = null; + for (final painter in _blockPainters) { + painter.dispose(); + } + _blockPainters = const []; + } +} diff --git a/lib/src/render/markdown_render_object.dart b/lib/src/render/markdown_render_object.dart new file mode 100644 index 0000000..7f4cad2 --- /dev/null +++ b/lib/src/render/markdown_render_object.dart @@ -0,0 +1,368 @@ +//ignore_for_file: unnecessary_import + +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart' show MouseTrackerAnnotation; +import 'package:meta/meta.dart' as meta show internal; + +import '../markdown.dart'; +import '../selection.dart'; +import '../theme.dart'; +import 'markdown_painter.dart'; + +/// Default color used to paint the selection highlight over the glyphs. +const Color _kSelectionColor = Color(0x552196F3); + +@meta.internal +class MarkdownRenderObject extends RenderBox + implements MarkdownSelectionSurface, MouseTrackerAnnotation { + MarkdownRenderObject({ + required Markdown markdown, + required MarkdownThemeData theme, + }) : _painter = MarkdownPainter( + markdown: markdown, + theme: theme, + ); + + /// Painter for rendering markdown content. + final MarkdownPainter _painter; + + /// The selection controller this render object participates in, if any. + MarkdownSelectionController? _controller; + + /// The stable document id used to anchor selection positions. + Object? _documentId; + + final Paint _highlightPaint = Paint()..color = _kSelectionColor; + + // Selection-handle leader layers pushed during paint so native handles + // (drawn by the scope's SelectionOverlay) follow the content as it scrolls. + LayerLink? _startHandleLink; + Offset? _startHandleLocal; + LayerLink? _endHandleLink; + Offset? _endHandleLocal; + + void _onSelectionChange() { + if (!_disposed) markNeedsPaint(); + } + + bool _disposed = false; + + void _attachController() { + final controller = _controller; + if (controller == null) return; + controller.addListener(_onSelectionChange); + if (attached) controller.attachSurface(this); + } + + void _detachController() { + final controller = _controller; + if (controller == null) return; + controller.removeListener(_onSelectionChange); + controller.detachSurface(this); + } + + /// Wires (or rewires) this render object to a selection [controller] under + /// [documentId]. Passing a null controller makes it non-selectable (inert). + @meta.internal + void updateSelection( + MarkdownSelectionController? controller, + Object? documentId, + ) { + if (identical(controller, _controller) && documentId == _documentId) return; + _detachController(); + _controller = controller; + _documentId = documentId; + _attachController(); + if (attached) { + markNeedsCompositingBitsUpdate(); + markNeedsPaint(); + } + } + + // --- MarkdownSelectionSurface --- + + @override + Object get documentId => _documentId!; + + @override + Rect get globalBounds => localToGlobal(Offset.zero) & size; + + @override + MarkdownPosition? positionForGlobal(Offset globalPosition) { + final id = _documentId; + if (id == null) return null; + final local = globalToLocal(globalPosition); + final hit = _painter.positionForLocal(local); + if (hit == null) return null; + return MarkdownPosition( + documentId: id, + blockIndex: hit.$1, + offset: hit.$2, + ); + } + + @override + (int, int, int)? wordBoundaryForGlobal(Offset globalPosition) { + if (_documentId == null) return null; + final wb = _painter.wordBoundaryForLocal(globalToLocal(globalPosition)); + if (wb == null) return null; + return (wb.$1, wb.$2.start, wb.$2.end); + } + + @override + List globalSelectionRects() { + final local = localSelectionRects(); + if (local.isEmpty) return const []; + final origin = localToGlobal(Offset.zero); + return [for (final rect in local) rect.shift(origin)]; + } + + @override + List localSelectionRects() { + final controller = _controller; + final id = _documentId; + if (controller == null || id == null) return const []; + return _painter.selectionBoxes((s) => controller.rangeFor(id, s)); + } + + @override + void setSelectionHandleLayers({ + LayerLink? startLink, + Offset? startLocal, + LayerLink? endLink, + Offset? endLocal, + }) { + var changed = false; + if (!identical(startLink, _startHandleLink) || + startLocal != _startHandleLocal) { + _startHandleLink = startLink; + _startHandleLocal = startLocal; + changed = true; + } + if (!identical(endLink, _endHandleLink) || endLocal != _endHandleLocal) { + _endHandleLink = endLink; + _endHandleLocal = endLocal; + changed = true; + } + if (changed && !_disposed && attached) markNeedsPaint(); + } + + @override + void repaintSelection() { + if (!_disposed && attached) markNeedsPaint(); + } + + // --- MouseTrackerAnnotation (I-beam cursor over selectable text) --- + + /// Whether the pointer is currently hovering an actionable link (updated in + /// [handleEvent]); drives the click (hand) cursor. + bool _hoverLink = false; + + /// Presents the click (hand) cursor over links, the text (I-beam) cursor + /// while this document participates in a selection controller (so users see + /// the content is selectable), and otherwise defers to what is behind it. + @override + MouseCursor get cursor { + if (_hoverLink) return SystemMouseCursors.click; + return _controller != null ? SystemMouseCursors.text : MouseCursor.defer; + } + + @override + void Function(PointerEnterEvent)? get onEnter => null; + + @override + void Function(PointerExitEvent)? get onExit => null; + + @override + bool get validForMouseTracker => !_disposed && attached; + + /// Current size of the render box. + @override + Size get size => _size; + Size _size = Size.zero; + + @override + bool get isRepaintBoundary => _controller != null; + + @override + bool get alwaysNeedsCompositing => false; + + @override + bool get sizedByParent => false; + + @override + set size(Size value) { + final prev = super.hasSize ? super.size : null; + super.size = value; + if (prev == value) return; + _size = value; + } + + @override + void debugResetSize() { + super.debugResetSize(); + if (!super.hasSize) return; + _size = super.size; + } + + // Measuring the content requires laying out the block [TextPainter]s, so this + // delegates to `_painter.layout(...)` which populates the painter's cached + // layout as a side effect (not a "pure" dry layout). [performLayout] re-runs + // the same layout, so the cached state is always finalized before [paint]. + @override + Size computeDryLayout(BoxConstraints constraints) => + constraints.constrain(_painter.layout(maxWidth: constraints.maxWidth)); + + @override + void performLayout() { + // Set the size of the render box to match the painter's size. + size = + constraints.constrain(_painter.layout(maxWidth: constraints.maxWidth)); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool hitTestChildren( + BoxHitTestResult result, { + required Offset position, + }) => + false; + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + var hitTarget = false; + if (size.contains(position)) { + hitTarget = hitTestSelf(position); + result.add(BoxHitTestEntry(this, position)); + } + return hitTarget; + } + + @override + void handleEvent(PointerEvent event, BoxHitTestEntry entry) { + // Track hover so the cursor can switch to the hand over links. A repaint is + // what prompts MouseTracker to re-read [cursor] (same mechanism as + // RenderMouseRegion); we only repaint when the link state actually flips. + if (event is PointerHoverEvent) { + final link = _painter.isLinkAtLocal(event.localPosition); + if (link != _hoverLink) { + _hoverLink = link; + if (!_disposed && attached) markNeedsPaint(); + } + } + _painter.handleEvent(event); + } + + /// Handles system font changes by marking the render object as needing layout + void _handleSystemFontsChange() { + // Invalidate cached layouts in painter and all block painters + _painter.invalidateLayout(); + // Request new layout and paint + markNeedsLayout(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + PaintingBinding.instance.systemFonts.addListener(_handleSystemFontsChange); + _controller?.attachSurface(this); + } + + /// Updates the render object with a new values. + /// This method should be called whenever the markdown or theme changes. + @meta.internal + void update({ + required Markdown markdown, + required MarkdownThemeData theme, + }) { + if (_painter.update( + markdown: markdown, + theme: theme, + )) { + // Mark the render object as needing layout. + markNeedsLayout(); + } + } + + @override + @protected + void detach() { + PaintingBinding.instance.systemFonts + .removeListener(_handleSystemFontsChange); + _controller?.detachSurface(this); + super.detach(); + } + + @override + @protected + void dispose() { + _disposed = true; + _controller?.removeListener(_onSelectionChange); + super.dispose(); + _painter.dispose(); + } + + @override + @protected + void paint(PaintingContext context, Offset offset) { + if (_painter.isEmpty) + return; // If the markdown is empty, do not paint anything. + + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + //..clipRect(Rect.fromLTWH(0, 0, size.width, size.height)); + + _painter.paint(canvas, size); + + // Paint the selection highlight OUTSIDE the cached content Picture, but ON + // TOP of the glyphs, so a translucent highlight stays visible even over + // opaque block/inline backgrounds (code fences, `inline code`, ==mark==). + // Still outside the Picture, so drag/streaming repaints never rebuild the + // glyph cache (the S7 invariant holds). + final controller = _controller; + final id = _documentId; + if (controller != null && id != null) { + _highlightPaint.color = controller.selectionColor ?? _kSelectionColor; + _painter.paintHighlight( + canvas, + (source) => controller.rangeFor(id, source), + _highlightPaint, + ); + } + + canvas.restore(); + + // Push handle leader layers (empty layers) so the scope's SelectionOverlay + // handles follow this content as it scrolls. + final startLink = _startHandleLink; + final startLocal = _startHandleLocal; + if (startLink != null && startLocal != null) { + context.pushLayer( + LeaderLayer(link: startLink, offset: offset + startLocal), + _paintNothing, + Offset.zero, + ); + } + final endLink = _endHandleLink; + final endLocal = _endHandleLocal; + if (endLink != null && endLocal != null) { + context.pushLayer( + LeaderLayer(link: endLink, offset: offset + endLocal), + _paintNothing, + Offset.zero, + ); + } + } +} + +/// A no-op paint callback for pushing childless [LeaderLayer]s. +void _paintNothing(PaintingContext context, Offset offset) {} diff --git a/lib/src/render/span_builder.dart b/lib/src/render/span_builder.dart new file mode 100644 index 0000000..1adcc56 --- /dev/null +++ b/lib/src/render/span_builder.dart @@ -0,0 +1,54 @@ +//ignore_for_file: unnecessary_import + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import '../nodes.dart'; +import '../theme.dart'; + +/// Builds a tap recognizer for the given markdown span. +TapGestureRecognizer? _buildTapRecognizer( + MD$Span span, + void Function(String title, String url)? onTap, +) { + if (onTap == null) return null; + if (span.extra case {'url': String url}) { + return TapGestureRecognizer() + ..onTap = () { + onTap(span.extra?['alt']?.toString() ?? span.text, url); + }; + } + return null; +} + +/// Helper function to create a [TextSpan] from markdown spans. +/// This function filters the spans based on the theme's span filter, +/// and applies the appropriate text style to each span. +TextSpan paragraphFromMarkdownSpans({ + required Iterable spans, + required MarkdownThemeData theme, + TextStyle? textStyle, +}) { + final style = textStyle ?? theme.textStyle; + final spanFilter = theme.spanFilter; + final filtered = spanFilter != null ? spans.where(spanFilter) : spans; + // With an explicit block style, merge it under each span's own style; + // otherwise the span style stands alone (the block style is applied once, on + // the parent TextSpan below). + final merge = textStyle != null; + TextSpan mapper(MD$Span span) => TextSpan( + text: span.text, + style: merge + ? theme.textStyleFor(span.style).merge(style) + : theme.textStyleFor(span.style), + recognizer: span.style.contains(MD$Style.link) + ? _buildTapRecognizer(span, theme.onLinkTap) + : null, + ); + return TextSpan( + style: textStyle ?? theme.textStyle, + children: filtered.map(mapper).toList(growable: false), + ); +} diff --git a/lib/src/selection.dart b/lib/src/selection.dart new file mode 100644 index 0000000..8422f8a --- /dev/null +++ b/lib/src/selection.dart @@ -0,0 +1,1341 @@ +import 'dart:ui' show Color, Offset, Rect, TextRange; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart' show LayerLink; + +import 'markdown.dart'; +import 'nodes.dart'; + +/// Rendered plain text of a single [MD$Block] โ€” the concatenation of its span +/// texts, in the coordinate space that `TextPainter.getPositionForOffset` +/// indexes. This is the single source of truth shared by selection extraction +/// and pointer hit-testing. +/// +/// Structural blocks ([MD$Divider], [MD$Spacer]) contribute no text. Lists join +/// items (and nested items, depth-first) with `\n`; tables join cells with `\t` +/// and rows with `\n`. +String markdownBlockRenderedText(MD$Block block) => block.map( + paragraph: (p) => _spans(p.spans), + heading: (h) => _spans(h.spans), + quote: (q) => _spans(q.spans), + alert: (a) => _spans(a.spans), + code: (c) => c.text, + list: (l) { + final parts = []; + _collectListItems(l.items, parts); + return parts.join('\n'); + }, + table: (t) => [ + t.header.cells.map(_spans).join('\t'), + for (final row in t.rows) row.cells.map(_spans).join('\t'), + ].join('\n'), + divider: (_) => '', + spacer: (_) => '', + ); + +String _spans(List spans) { + final buffer = StringBuffer(); + for (final span in spans) buffer.write(span.text); + return buffer.toString(); +} + +// Flattens list items depth-first into one text run per item. Joined with `\n` +// unconditionally (one separator between every item, matching the painter's +// per-item fragments) so empty items still occupy their own line. +void _collectListItems(List items, List out) { + for (final item in items) { + out.add(_spans(item.spans)); + if (item.children.isNotEmpty) _collectListItems(item.children, out); + } +} + +/// A logical caret position inside a selectable Markdown document. +/// +/// Anchored on the immutable model โ€” never on a mounted render object โ€” so it +/// stays valid while the widget is scrolled off-screen and disposed. +@immutable +final class MarkdownPosition { + /// Creates a position in [documentId] at rendered-text [offset] of the block + /// at [blockIndex] in `Markdown.blocks`. + const MarkdownPosition({ + required this.documentId, + required this.blockIndex, + required this.offset, + }); + + /// Stable id of the document (e.g. a chat message id). Opaque to the library. + final Object documentId; + + /// Index into the source `Markdown.blocks` (not the painter list). + final int blockIndex; + + /// Offset into the block's rendered text (see [markdownBlockRenderedText]). + final int offset; + + /// A copy with the given fields replaced. + MarkdownPosition copyWith({ + Object? documentId, + int? blockIndex, + int? offset, + }) => + MarkdownPosition( + documentId: documentId ?? this.documentId, + blockIndex: blockIndex ?? this.blockIndex, + offset: offset ?? this.offset, + ); + + @override + bool operator ==(Object other) => + other is MarkdownPosition && + other.documentId == documentId && + other.blockIndex == blockIndex && + other.offset == offset; + + @override + int get hashCode => Object.hash(documentId, blockIndex, offset); + + @override + String toString() => 'MarkdownPosition($documentId#$blockIndex@$offset)'; +} + +/// A directed selection between [base] (where the gesture anchored) and +/// [extent] (the moving end). Reading order is resolved by the controller. +@immutable +final class MarkdownSelection { + /// Creates a selection from [base] to [extent]. + const MarkdownSelection({required this.base, required this.extent}); + + /// A collapsed (empty) selection at [at]. + const MarkdownSelection.collapsed(MarkdownPosition at) + : base = at, + extent = at; + + /// The fixed anchor of the selection. + final MarkdownPosition base; + + /// The moving end of the selection. + final MarkdownPosition extent; + + /// Whether [base] and [extent] coincide (nothing is selected). + bool get isCollapsed => base == extent; + + @override + bool operator ==(Object other) => + other is MarkdownSelection && + other.base == base && + other.extent == extent; + + @override + int get hashCode => Object.hash(base, extent); + + @override + String toString() => 'MarkdownSelection($base -> $extent)'; +} + +/// A document registered with a controller, in reading order. +@immutable +final class MarkdownDocumentRef { + /// Creates a reference binding a stable [id] to its immutable [model]. + const MarkdownDocumentRef( + {required this.id, required this.model, this.order}); + + /// Stable id of the document (e.g. a chat message id). + final Object id; + + /// The immutable Markdown model. Retained by the app; the controller reads + /// text from it even while the widget is unmounted. + final Markdown model; + + /// Explicit reading-order key. When null, registration order is used. Supply + /// it (e.g. the message index) so unmounted documents still order correctly. + final int? order; +} + +/// One block's contribution to a selection: the guaranteed [text] slice plus +/// structured metadata for custom formatters. +@immutable +final class MarkdownSelectedBlock { + /// Creates a selected-block segment. + const MarkdownSelectedBlock({ + required this.blockIndex, + required this.type, + required this.text, + required this.renderedRange, + required this.block, + this.sourceRange, + }); + + /// Index of the block in `Markdown.blocks`. + final int blockIndex; + + /// The block's `MD$Block.type` (`'paragraph'`, `'table'`, ...). + final String type; + + /// The selected slice of the block's rendered text. Always populated. + final String text; + + /// The selected range within the block's rendered text. + final TextRange renderedRange; + + /// Best-effort range within the block's Markdown source (may be null). + final TextRange? sourceRange; + + /// The immutable block, for consumers that reconstruct richer output. + final MD$Block block; +} + +/// One document's contribution to a selection. +@immutable +final class MarkdownSelectedDocument { + /// Creates a selected-document segment. + const MarkdownSelectedDocument({ + required this.documentId, + required this.blocks, + }); + + /// The document's stable id. + final Object documentId; + + /// The selected blocks of this document, in reading order. + final List blocks; +} + +/// The structured result of a selection, spanning one or more documents. +/// +/// This is the canonical representation; render it to a string with a +/// [MarkdownSelectionFormatter] (or the default [MarkdownPlainTextFormatter]). +@immutable +final class MarkdownSelectedContent { + /// Creates structured selected content. + const MarkdownSelectedContent({required this.documents}); + + /// The selected documents, in reading order. + final List documents; + + /// Whether nothing is selected. + bool get isEmpty => documents.isEmpty; + + /// Whether something is selected. + bool get isNotEmpty => documents.isNotEmpty; + + /// Convenience: format with the default [MarkdownPlainTextFormatter]. + String toPlainText() => const MarkdownPlainTextFormatter().format(this); +} + +/// Turns [MarkdownSelectedContent] into a string. Implement this to customize +/// how a selection is copied (e.g. "Copy as Markdown"). +abstract interface class MarkdownSelectionFormatter { + /// Formats [content] into a single string. + String format(MarkdownSelectedContent content); +} + +/// The default formatter: joins block slices with [blockSeparator] and +/// documents with [documentSeparator]. Structural blocks (empty text) are +/// skipped. Table/list cell structure is already baked into each block's text. +@immutable +final class MarkdownPlainTextFormatter implements MarkdownSelectionFormatter { + /// Creates a plain-text formatter. + const MarkdownPlainTextFormatter({ + this.blockSeparator = '\n', + this.documentSeparator = '\n\n', + }); + + /// Inserted between blocks within a document. + final String blockSeparator; + + /// Inserted between documents. + final String documentSeparator; + + @override + String format(MarkdownSelectedContent content) { + final docs = []; + for (final doc in content.documents) { + final blocks = []; + for (final block in doc.blocks) { + if (block.text.isEmpty) continue; + blocks.add(block.text); + } + if (blocks.isNotEmpty) docs.add(blocks.join(blockSeparator)); + } + return docs.join(documentSeparator); + } +} + +/// A formatter that reconstructs Markdown-flavored text from a selection, +/// preserving structure that [MarkdownPlainTextFormatter] flattens away: +/// heading levels (`#`), blockquote and alert prefixes (`>`), fenced code +/// (```` ``` ````), nested list markers with task checkboxes, and pipe tables. +/// +/// Fidelity is best-effort. A block is re-rendered from its model only when the +/// selection covers it **in full**; a partially selected boundary block falls +/// back to its plain sliced [MarkdownSelectedBlock.text], so the copied output +/// never leaks text from outside the selection (at the cost of losing markup on +/// just those edge blocks). This matches the common case โ€” selecting whole +/// lists, sections, or messages โ€” while staying safe on ragged edges. +@immutable +final class MarkdownMarkupFormatter implements MarkdownSelectionFormatter { + /// Creates a Markdown-reconstructing formatter. + const MarkdownMarkupFormatter({ + this.blockSeparator = '\n\n', + this.documentSeparator = '\n\n', + this.listIndent = ' ', + }); + + /// Inserted between blocks within a document (a blank line by default, the + /// idiomatic Markdown block separator). + final String blockSeparator; + + /// Inserted between documents. + final String documentSeparator; + + /// Whitespace prepended per nesting level of a list. Two spaces by default. + final String listIndent; + + @override + String format(MarkdownSelectedContent content) { + final docs = []; + for (final doc in content.documents) { + final blocks = []; + for (final block in doc.blocks) { + final rendered = _block(block); + if (rendered.isNotEmpty) blocks.add(rendered); + } + if (blocks.isNotEmpty) docs.add(blocks.join(blockSeparator)); + } + return docs.join(documentSeparator); + } + + String _block(MarkdownSelectedBlock seg) { + // Only reconstruct rich markup when the whole block is selected; a partial + // boundary block falls back to its plain sliced text so we never emit text + // outside the selection. + final full = markdownBlockRenderedText(seg.block); + final whole = + seg.renderedRange.start <= 0 && seg.renderedRange.end >= full.length; + if (!whole) return seg.text; + return seg.block.map( + paragraph: (p) => _spans(p.spans), + heading: (h) => '${'#' * h.level.clamp(1, 6)} ${_spans(h.spans)}', + quote: (q) => _prefixLines(_spans(q.spans), '> '), + alert: (a) => + '> [!${a.alert.marker}]\n${_prefixLines(_spans(a.spans), '> ')}', + code: (c) => '```${c.language ?? ''}\n${c.text}\n```', + list: (l) => _list(l.items, 0), + table: _table, + divider: (_) => '---', + spacer: (_) => '', + ); + } + + String _list(List items, int depth) { + final out = []; + for (final item in items) { + final box = item.isTask ? (item.checked! ? '[x] ' : '[ ] ') : ''; + out.add('${listIndent * depth}${item.marker} $box${_spans(item.spans)}'); + if (item.children.isNotEmpty) out.add(_list(item.children, depth + 1)); + } + return out.join('\n'); + } + + String _table(MD$Table t) { + String row(MD$TableRow r) => '| ${r.cells.map(_spans).join(' | ')} |'; + final cols = t.header.cells.length; + return [ + row(t.header), + '| ${List.filled(cols, '---').join(' | ')} |', + for (final r in t.rows) row(r), + ].join('\n'); + } + + String _prefixLines(String text, String prefix) => + text.split('\n').map((line) => '$prefix$line').join('\n'); +} + +/// Decides how a selection anchor is remapped when a document's model is +/// replaced (e.g. streaming). Returning null drops the anchor (collapsing the +/// selection). No stable block id is required โ€” remapping is content-based. +abstract interface class MarkdownReconciliationPolicy { + /// Append-only fast path, else clamp indices/offsets into the new bounds. + /// Cheapest; correct for streaming appends, drifts on front/mid inserts. + const factory MarkdownReconciliationPolicy.appendFastPath() = + _AppendFastPathPolicy; + + /// Append fast path, then relocate by matching block rendered text, else + /// clamp. The default โ€” robust to inserts/reorders without a model id. + const factory MarkdownReconciliationPolicy.contentAnchored() = + _ContentAnchoredPolicy; + + /// Drop the selection whenever the anchor's document changes at all. + const factory MarkdownReconciliationPolicy.clearOnChange() = _ClearPolicy; + + /// Remaps [anchor] from [oldModel] to [newModel]; null drops it. + MarkdownPosition? remap( + MarkdownPosition anchor, + Markdown oldModel, + Markdown newModel, + ); +} + +MarkdownPosition _clampInto(MarkdownPosition anchor, Markdown model) { + if (model.blocks.isEmpty) { + return MarkdownPosition( + documentId: anchor.documentId, blockIndex: 0, offset: 0); + } + final bi = anchor.blockIndex.clamp(0, model.blocks.length - 1); + final len = markdownBlockRenderedText(model.blocks[bi]).length; + return MarkdownPosition( + documentId: anchor.documentId, + blockIndex: bi, + offset: anchor.offset.clamp(0, len), + ); +} + +bool _appendPrefixKeeps(MarkdownPosition anchor, Markdown o, Markdown n) { + if (anchor.blockIndex >= o.blocks.length || + anchor.blockIndex >= n.blocks.length) { + return false; + } + for (var i = 0; i < anchor.blockIndex; i++) { + if (i >= n.blocks.length || + markdownBlockRenderedText(o.blocks[i]) != + markdownBlockRenderedText(n.blocks[i])) { + return false; + } + } + final oldText = markdownBlockRenderedText(o.blocks[anchor.blockIndex]); + final newText = markdownBlockRenderedText(n.blocks[anchor.blockIndex]); + return newText.startsWith(oldText) || oldText.startsWith(newText); +} + +@immutable +class _AppendFastPathPolicy implements MarkdownReconciliationPolicy { + const _AppendFastPathPolicy(); + @override + MarkdownPosition? remap(MarkdownPosition anchor, Markdown o, Markdown n) { + if (_appendPrefixKeeps(anchor, o, n)) { + final len = markdownBlockRenderedText(n.blocks[anchor.blockIndex]).length; + return MarkdownPosition( + documentId: anchor.documentId, + blockIndex: anchor.blockIndex, + offset: anchor.offset.clamp(0, len), + ); + } + return _clampInto(anchor, n); + } +} + +@immutable +class _ContentAnchoredPolicy implements MarkdownReconciliationPolicy { + const _ContentAnchoredPolicy(); + @override + MarkdownPosition? remap(MarkdownPosition anchor, Markdown o, Markdown n) { + if (anchor.blockIndex >= o.blocks.length) return _clampInto(anchor, n); + if (_appendPrefixKeeps(anchor, o, n)) { + final len = markdownBlockRenderedText(n.blocks[anchor.blockIndex]).length; + return MarkdownPosition( + documentId: anchor.documentId, + blockIndex: anchor.blockIndex, + offset: anchor.offset.clamp(0, len), + ); + } + // Relocate by matching the anchor block's rendered text in the new model. + final oldText = markdownBlockRenderedText(o.blocks[anchor.blockIndex]); + if (oldText.isNotEmpty) { + for (var i = 0; i < n.blocks.length; i++) { + if (markdownBlockRenderedText(n.blocks[i]) == oldText) { + return MarkdownPosition( + documentId: anchor.documentId, + blockIndex: i, + offset: anchor.offset.clamp(0, oldText.length), + ); + } + } + } + return _clampInto(anchor, n); + } +} + +@immutable +class _ClearPolicy implements MarkdownReconciliationPolicy { + const _ClearPolicy(); + @override + MarkdownPosition? remap(MarkdownPosition anchor, Markdown o, Markdown n) => + null; +} + +/// A mounted document's geometry bridge โ€” the controller's window onto a live +/// render object. Implemented by the render layer; used for hit-testing. +abstract interface class MarkdownSelectionSurface { + /// The document id this surface renders. + Object get documentId; + + /// The surface's bounds in global (screen) coordinates. + Rect get globalBounds; + + /// Maps a global point to a logical position, or null if outside any text. + MarkdownPosition? positionForGlobal(Offset globalPosition); + + /// The word range at a global point, as `(blockIndex, start, end)` in the + /// block's rendered-text space, using the platform word segmentation. Null + /// when the point misses selectable text. + (int, int, int)? wordBoundaryForGlobal(Offset globalPosition); + + /// Global (screen-space) rectangles covering the selected part of this + /// surface's document, in reading order. Empty when nothing here is + /// selected. Used to place selection handles, the magnifier and the toolbar + /// anchor. + List globalSelectionRects(); + + /// Content-local rectangles covering the selected part of this surface's + /// document, in reading order. The local-space twin of [globalSelectionRects] + /// used to anchor handle leader layers. + List localSelectionRects(); + + /// Sets the selection-handle leader layers this surface paints, so the + /// scope's `SelectionOverlay` handles follow the content. Pass a [startLink] + /// or [endLink] with its content-local anchor; pass null to remove a handle. + void setSelectionHandleLayers({ + LayerLink? startLink, + Offset? startLocal, + LayerLink? endLink, + Offset? endLocal, + }); + + /// Requests a repaint of just the selection highlight (e.g. after a color + /// change). Safe to call during a build phase (schedules paint, not build). + void repaintSelection(); +} + +/// The mounted geometry at the two ends of a selection, for placing handles. +@immutable +class MarkdownHandleEndpoints { + /// Creates endpoints binding each edge to its owning surface with the local + /// and global caret rects at that edge. + const MarkdownHandleEndpoints({ + required this.startSurface, + required this.startLocal, + required this.startGlobal, + required this.endSurface, + required this.endLocal, + required this.endGlobal, + }); + + /// The surface owning the reading-order start edge. + final MarkdownSelectionSurface startSurface; + + /// The local caret rect at the start edge (within [startSurface]). + final Rect startLocal; + + /// The global caret rect at the start edge. + final Rect startGlobal; + + /// The surface owning the reading-order end edge. + final MarkdownSelectionSurface endSurface; + + /// The local caret rect at the end edge (within [endSurface]). + final Rect endLocal; + + /// The global caret rect at the end edge. + final Rect endGlobal; +} + +class _DocEntry { + _DocEntry(this.id, this.model, this.order); + final Object id; + Markdown model; + int order; +} + +/// The single source of truth for a Markdown selection. +/// +/// Holds the selection as logical anchors over an app-supplied registry of +/// immutable models, so selected text can always be extracted โ€” even for +/// documents whose widgets are currently unmounted (e.g. scrolled out of a +/// chat list). Mounted render objects register as [MarkdownSelectionSurface]s +/// for hit-testing and listen for repaints. +class MarkdownSelectionController extends ChangeNotifier { + /// Creates a controller with an optional [reconciliation] policy (defaults to + /// [MarkdownReconciliationPolicy.contentAnchored]) and default [formatter]. + MarkdownSelectionController({ + MarkdownReconciliationPolicy? reconciliation, + MarkdownSelectionFormatter formatter = const MarkdownPlainTextFormatter(), + MarkdownSelectionGroup? group, + }) : reconciliation = reconciliation ?? + const MarkdownReconciliationPolicy.contentAnchored(), + _formatter = formatter, + _group = group { + group?._add(this); + } + + /// The anchor-remapping policy used on document updates. + final MarkdownReconciliationPolicy reconciliation; + + final MarkdownSelectionGroup? _group; + + MarkdownSelectionFormatter _formatter; + + /// The default formatter used by [getText] when none is supplied. + MarkdownSelectionFormatter get formatter => _formatter; + set formatter(MarkdownSelectionFormatter value) { + if (identical(value, _formatter)) return; + _formatter = value; + notifyListeners(); + } + + Color? _selectionColor; + + /// The color of the selection highlight, or null to use the render layer's + /// default. Usually set by [MarkdownSelectionScope] from the ambient + /// `DefaultSelectionStyle` / `TextSelectionTheme`. + /// + /// Changing it repaints mounted surfaces directly rather than notifying + /// listeners โ€” it is a rendering detail, not a selection change, and is often + /// set during a build phase (from `didChangeDependencies`). + Color? get selectionColor => _selectionColor; + set selectionColor(Color? value) { + if (value == _selectionColor) return; + _selectionColor = value; + for (final surface in _surfaces.values) { + surface.repaintSelection(); + } + } + + final List<_DocEntry> _docs = <_DocEntry>[]; + + /// id โ†’ index into the sorted [_docs], kept in sync by [_reindex]. Makes + /// [_orderIndex] O(1): it is called several times per selectable block on + /// every highlight repaint, so a linear scan here would scale with the chat + /// size and show up during drags. + final Map _indexById = {}; + + final Map _surfaces = + {}; + + MarkdownSelection? _selection; + + /// The current selection, or null when nothing is selected. + MarkdownSelection? get selection => _selection; + set selection(MarkdownSelection? value) { + if (value == _selection) return; + _selection = value; + if (value != null && !value.isCollapsed) _group?._claim(this); + notifyListeners(); + } + + @override + void dispose() { + _group?._remove(this); + super.dispose(); + } + + /// The number of registered documents. Prefer this over `documents.length`, + /// which allocates a fresh list on every read. + int get documentCount => _docs.length; + + /// Whether any documents are registered. + bool get hasDocuments => _docs.isNotEmpty; + + /// The registered documents, in reading order. + List get documents => [ + for (final e in _docs) + MarkdownDocumentRef(id: e.id, model: e.model, order: e.order), + ]; + + // --- registry ----------------------------------------------------------- + + /// Replaces the whole registry (initial/bulk load), preserving a still-valid + /// selection by clamping it into the new documents. + void setDocuments(Iterable docs) { + _docs + ..clear() + ..addAll(<_DocEntry>[ + for (final (i, d) in docs.indexed) + _DocEntry(d.id, d.model, d.order ?? i), + ]); + _sort(); + _validateSelection(); + notifyListeners(); + } + + /// Inserts or updates one document. On a model change the selection is + /// reconciled via [reconciliation] (this is the streaming entry point). + void putDocument(Object id, Markdown model, {int? order}) { + final idx = _orderIndex(id); + if (idx < 0) { + _docs.add(_DocEntry(id, model, order ?? _docs.length)); + _sort(); + notifyListeners(); + return; + } + final entry = _docs[idx]; + final old = entry.model; + final orderChanged = order != null && order != entry.order; + final modelChanged = !identical(old, model); + // Nothing actually changed โ€” avoid a needless sort/repaint. This matters on + // the streaming path, which can call putDocument once per token. + if (!orderChanged && !modelChanged) return; + if (orderChanged) entry.order = order; + if (modelChanged) entry.model = model; + // Reordering shifts indices; a model-only update keeps [_indexById] valid. + if (orderChanged) _sort(); + if (modelChanged) _reconcile(id, old, model); + notifyListeners(); + } + + /// Removes a document. If the selection touched it, the selection is dropped. + void removeDocument(Object id) { + _docs.removeWhere((e) => e.id == id); + _reindex(); + final sel = _selection; + if (sel != null && + (sel.base.documentId == id || sel.extent.documentId == id)) { + _selection = null; + } + notifyListeners(); + } + + // Documents are ordered by their `order` key. `List.sort` is not stable, so + // documents sharing an identical `order` have unspecified relative order โ€” + // callers should supply unique `order` values (e.g. the message index). + void _sort() { + _docs.sort((a, b) => a.order.compareTo(b.order)); + _reindex(); + } + + /// Rebuilds [_indexById] from the current [_docs] order. Called whenever the + /// set or order of documents changes (not on model-only updates). + void _reindex() { + _indexById.clear(); + for (var i = 0; i < _docs.length; i++) { + _indexById[_docs[i].id] = i; + } + } + + void _reconcile(Object id, Markdown oldModel, Markdown newModel) { + final sel = _selection; + if (sel == null) return; + final base = sel.base.documentId == id + ? reconciliation.remap(sel.base, oldModel, newModel) + : sel.base; + final extent = sel.extent.documentId == id + ? reconciliation.remap(sel.extent, oldModel, newModel) + : sel.extent; + _selection = (base == null || extent == null) + ? null + : MarkdownSelection(base: base, extent: extent); + } + + void _validateSelection() { + final sel = _selection; + if (sel == null) return; + if (_orderIndex(sel.base.documentId) < 0 || + _orderIndex(sel.extent.documentId) < 0) { + _selection = null; + return; + } + _selection = MarkdownSelection( + base: _clampInto(sel.base, _modelOf(sel.base.documentId)), + extent: _clampInto(sel.extent, _modelOf(sel.extent.documentId)), + ); + } + + // --- surfaces (mounted geometry) ---------------------------------------- + + /// Registers a mounted [surface] for hit-testing. Called on RenderObject + /// attach. + void attachSurface(MarkdownSelectionSurface surface) { + _surfaces[surface.documentId] = surface; + } + + /// Unregisters a [surface]. Called on RenderObject detach/dispose. + void detachSurface(MarkdownSelectionSurface surface) { + if (identical(_surfaces[surface.documentId], surface)) { + _surfaces.remove(surface.documentId); + } + } + + /// The currently mounted surfaces. + Iterable get mountedSurfaces => _surfaces.values; + + /// Maps a global point to a logical position by asking mounted surfaces. + /// + /// When the point is inside a surface it is used directly; otherwise the + /// vertically-nearest surface is chosen and the point clamped into it, so a + /// drag through the gaps/edges between widgets still extends the selection. + MarkdownPosition? positionForGlobal(Offset globalPosition) { + final resolved = _resolveSurfaceAt(globalPosition); + return resolved?.$1.positionForGlobal(resolved.$2); + } + + /// Resolves the surface a global point belongs to (directly when inside, + /// otherwise the vertically-nearest one) together with the point clamped into + /// that surface. Shared by [positionForGlobal] and [wordSelectionAt] so a + /// gesture through the gaps between widgets still resolves consistently. + (MarkdownSelectionSurface, Offset)? _resolveSurfaceAt(Offset globalPosition) { + MarkdownSelectionSurface? nearest; + var bestDistance = double.infinity; + for (final surface in _surfaces.values) { + final bounds = surface.globalBounds; + // A zero-area surface (an empty document, or one that lays out to zero + // width/height) has nothing to select and would invert the clamp below. + if (bounds.isEmpty) continue; + if (bounds.contains(globalPosition)) return (surface, globalPosition); + final dy = globalPosition.dy < bounds.top + ? bounds.top - globalPosition.dy + : (globalPosition.dy > bounds.bottom + ? globalPosition.dy - bounds.bottom + : 0.0); + if (dy < bestDistance) { + bestDistance = dy; + nearest = surface; + } + } + if (nearest == null) return null; + final bounds = nearest.globalBounds; + // Clamp INTO the surface, keeping the upper bound >= the lower bound so a + // very small surface never inverts the limits (num.clamp throws then). + final maxX = bounds.right - 0.01; + final maxY = bounds.bottom - 0.01; + final clamped = Offset( + globalPosition.dx + .clamp(bounds.left, maxX < bounds.left ? bounds.left : maxX), + globalPosition.dy + .clamp(bounds.top, maxY < bounds.top ? bounds.top : maxY), + ); + return (nearest, clamped); + } + + // --- mutation ------------------------------------------------------------ + + /// Clears the selection. + void clear() => selection = null; + + /// Collapses the selection at [position]. + void collapseAt(MarkdownPosition position) => + selection = MarkdownSelection.collapsed(position); + + /// Extends the moving end of the selection to [position] (anchoring [base] + /// first if there is no selection yet). + void extendTo(MarkdownPosition position) { + final sel = _selection; + selection = sel == null + ? MarkdownSelection.collapsed(position) + : MarkdownSelection(base: sel.base, extent: position); + } + + /// Begins a selection at a global point (e.g. a drag start). + void startAtGlobal(Offset globalPosition) { + final p = positionForGlobal(globalPosition); + if (p != null) collapseAt(p); + } + + /// Extends the selection to a global point (e.g. a drag update). + void extendToGlobal(Offset globalPosition) { + final p = positionForGlobal(globalPosition); + if (p != null) extendTo(p); + } + + /// Selects everything across every registered document. + void selectAll() { + if (_docs.isEmpty) return; + final first = _docs.first, last = _docs.last; + if (first.model.blocks.isEmpty || last.model.blocks.isEmpty) return; + final lastBlock = last.model.blocks.length - 1; + selection = MarkdownSelection( + base: MarkdownPosition(documentId: first.id, blockIndex: 0, offset: 0), + extent: MarkdownPosition( + documentId: last.id, + blockIndex: lastBlock, + offset: markdownBlockRenderedText(last.model.blocks[lastBlock]).length, + ), + ); + } + + // --- word / block selection (multi-tap, long-press) ---------------------- + + /// The word range around [offset] in a block's rendered [text], as + /// `(start, end)`. A "word" is the maximal run of same-class characters + /// (letters/digits vs. punctuation vs. whitespace) touching the caret, + /// preferring an adjacent word character so clicking a word's edge grabs it. + /// + /// Exposed for testing; mirrors what double-click / long-press select. + /// + /// Limitation (v1): the block's rendered text is indexed by UTF-16 code unit, + /// so word segmentation may split a surrogate pair (e.g. an emoji or other + /// non-BMP character). Accepted for v1. + @visibleForTesting + static (int, int) wordRangeIn(String text, int offset) { + final len = text.length; + if (len == 0) return (0, 0); + final o = offset.clamp(0, len); + // Reference character: a word char adjacent to the caret (right first, then + // left), else whichever side has a character at all. + final int idx; + if (o < len && _charClass(text.codeUnitAt(o)) == _clsWord) { + idx = o; + } else if (o > 0 && _charClass(text.codeUnitAt(o - 1)) == _clsWord) { + idx = o - 1; + } else if (o < len) { + idx = o; + } else { + idx = o - 1; + } + final cls = _charClass(text.codeUnitAt(idx)); + var start = idx, end = idx + 1; + while (start > 0 && _charClass(text.codeUnitAt(start - 1)) == cls) start--; + while (end < len && _charClass(text.codeUnitAt(end)) == cls) end++; + return (start, end); + } + + static const int _clsSpace = 0; + static const int _clsWord = 1; + static const int _clsPunct = 2; + + static int _charClass(int c) { + if (c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D) return _clsSpace; + final isAsciiWord = (c >= 0x30 && c <= 0x39) || // 0-9 + (c >= 0x41 && c <= 0x5A) || // A-Z + (c >= 0x61 && c <= 0x7A) || // a-z + c == 0x5F; // _ + if (isAsciiWord) return _clsWord; + if (c < 0x80) return _clsPunct; // other ASCII => punctuation + return _clsWord; // non-ASCII (accents, CJK, emoji) => word-ish + } + + /// The ordered selection of the word at a global point, without applying it. + /// + /// Prefers the mounted painter's platform word segmentation + /// (`TextPainter.getWordBoundary`, keeping intra-word punctuation like + /// apostrophes); falls back to the text-based [wordRangeIn] heuristic when + /// the surface can't answer (e.g. an unmounted document during a drag). + MarkdownSelection? wordSelectionAt(Offset globalPosition) { + final resolved = _resolveSurfaceAt(globalPosition); + if (resolved == null) return null; + final (surface, point) = resolved; + final id = surface.documentId; + final wb = surface.wordBoundaryForGlobal(point); + if (wb != null) { + return MarkdownSelection( + base: + MarkdownPosition(documentId: id, blockIndex: wb.$1, offset: wb.$2), + extent: + MarkdownPosition(documentId: id, blockIndex: wb.$1, offset: wb.$3), + ); + } + // Fallback: text-heuristic boundary. + final p = surface.positionForGlobal(point); + if (p == null) return null; + final di = _orderIndex(p.documentId); + if (di < 0) return null; + final (s, e) = wordRangeIn(_blockTextAt(di, p.blockIndex), p.offset); + return MarkdownSelection( + base: p.copyWith(offset: s), + extent: p.copyWith(offset: e), + ); + } + + /// The ordered selection of the whole block at a global point, without + /// applying it. + MarkdownSelection? blockSelectionAt(Offset globalPosition) { + final p = positionForGlobal(globalPosition); + if (p == null) return null; + final di = _orderIndex(p.documentId); + if (di < 0) return null; + final text = _blockTextAt(di, p.blockIndex); + return MarkdownSelection( + base: p.copyWith(offset: 0), + extent: p.copyWith(offset: text.length), + ); + } + + /// Selects the word at a global point (double-click / long-press). Returns + /// the ordered anchor selection it applied, or null if the point misses. + MarkdownSelection? selectWordAtGlobal(Offset globalPosition) { + final sel = wordSelectionAt(globalPosition); + if (sel != null) selection = sel; + return sel; + } + + /// Selects the whole block (paragraph/line) at a global point (triple-click). + /// Returns the ordered anchor selection it applied, or null if it misses. + MarkdownSelection? selectBlockAtGlobal(Offset globalPosition) { + final sel = blockSelectionAt(globalPosition); + if (sel != null) selection = sel; + return sel; + } + + /// Extends a word/block-granular drag: grows the selection from the fixed + /// [anchor] (an ordered word/block range) to include the word (or block) at + /// [globalPosition], so double/triple-click-drag snaps to whole units. + void extendSelectionGranular( + MarkdownSelection anchor, + Offset globalPosition, { + required bool word, + }) { + final target = word + ? wordSelectionAt(globalPosition) + : blockSelectionAt(globalPosition); + if (target == null) return; + // anchor and target are each ordered (base <= extent). Keep the fixed + // anchor edge as the base and put the moving edge in extent. + if (_compare(target.base, anchor.base) < 0) { + selection = MarkdownSelection(base: anchor.extent, extent: target.base); + } else if (_compare(target.extent, anchor.extent) > 0) { + selection = MarkdownSelection(base: anchor.base, extent: target.extent); + } else { + selection = MarkdownSelection(base: anchor.base, extent: anchor.extent); + } + } + + // --- keyboard extension -------------------------------------------------- + + void _extendExtent(MarkdownPosition Function(MarkdownPosition) step) { + final sel = _selection; + if (sel == null) return; + final next = step(sel.extent); + if (next == sel.extent) return; + selection = MarkdownSelection(base: sel.base, extent: next); + } + + /// Extends the moving end of the selection by one character ([forward] = + /// toward the end of the text). No-op without a current selection. + void extendSelectionByCharacter({required bool forward}) => + _extendExtent((p) => _stepCharacter(p, forward: forward)); + + /// Extends the moving end of the selection by one word. + void extendSelectionByWord({required bool forward}) => + _extendExtent((p) => _stepWord(p, forward: forward)); + + /// Extends the moving end of the selection to the start/end of its block + /// (the closest analogue of a line-break extension for Markdown blocks). + void extendSelectionToLineBreak({required bool forward}) => + _extendExtent((p) => _stepLineBreak(p, forward: forward)); + + /// Extends the moving end of the selection to the very start of the first + /// document or the very end of the last one. + void extendSelectionToDocumentBoundary({required bool forward}) { + if (_docs.isEmpty) return; + _extendExtent((_) => _documentBoundary(forward: forward)); + } + + /// Extends the moving end of the selection to the adjacent visual line, using + /// on-screen geometry (falls back to nothing when the endpoint is unmounted). + void extendSelectionToAdjacentLine({required bool forward}) { + final sel = _selection; + if (sel == null) return; + final rects = globalSelectionRects(); + if (rects.isEmpty) return; + final (a, _) = _ordered(sel); + final rect = sel.extent == a ? rects.first : rects.last; + final target = Offset( + rect.center.dx, + rect.center.dy + (forward ? rect.height : -rect.height), + ); + final moved = positionForGlobal(target); + if (moved != null) { + selection = MarkdownSelection(base: sel.base, extent: moved); + } + } + + /// Moves one edge of the selection to a global point, keeping the other edge + /// fixed. Used by the draggable selection handles: [isStart] moves the + /// reading-order start edge, otherwise the end edge. + void moveSelectionEdgeToGlobal( + Offset globalPosition, { + required bool isStart, + }) { + final sel = _selection; + if (sel == null) return; + final moved = positionForGlobal(globalPosition); + if (moved == null) return; + final (a, b) = _ordered(sel); + final next = isStart + ? MarkdownSelection(base: b, extent: moved) + : MarkdownSelection(base: a, extent: moved); + // Don't let a handle drag collapse the selection out from under itself + // (that would dispose the overlay mid-gesture); keep one caret gap. + if (next.isCollapsed) return; + selection = next; + } + + // --- geometry (mounted surfaces) ----------------------------------------- + + /// Global rects covering the current selection across every mounted surface, + /// in reading order. Unmounted documents contribute nothing (they have no + /// geometry). Empty when the selection is collapsed or entirely off-screen. + List globalSelectionRects() { + final sel = _selection; + if (sel == null || sel.isCollapsed) return const []; + final (a, b) = _ordered(sel); + final startDoc = _orderIndex(a.documentId); + final endDoc = _orderIndex(b.documentId); + if (startDoc < 0 || endDoc < 0) return const []; + final out = []; + for (var d = startDoc; d <= endDoc; d++) { + final surface = _surfaces[_docs[d].id]; + if (surface == null) continue; + out.addAll(surface.globalSelectionRects()); + } + return out; + } + + /// Resolves the mounted surfaces and local/global caret rects at the two ends + /// of the current selection, for placing selection handles. Null when the + /// selection is collapsed or neither end is mounted. + MarkdownHandleEndpoints? selectionHandleEndpoints() { + final sel = _selection; + if (sel == null || sel.isCollapsed) return null; + final (a, b) = _ordered(sel); + final startDoc = _orderIndex(a.documentId); + final endDoc = _orderIndex(b.documentId); + if (startDoc < 0 || endDoc < 0) return null; + MarkdownSelectionSurface? startSurface, endSurface; + Rect? startLocal, startGlobal, endLocal, endGlobal; + for (var d = startDoc; d <= endDoc; d++) { + final surface = _surfaces[_docs[d].id]; + if (surface == null) continue; + final local = surface.localSelectionRects(); + if (local.isEmpty) continue; + final global = surface.globalSelectionRects(); + if (global.length != local.length) continue; + if (startSurface == null) { + startSurface = surface; + startLocal = local.first; + startGlobal = global.first; + } + endSurface = surface; + endLocal = local.last; + endGlobal = global.last; + } + if (startSurface == null || endSurface == null) return null; + return MarkdownHandleEndpoints( + startSurface: startSurface, + startLocal: startLocal!, + startGlobal: startGlobal!, + endSurface: endSurface, + endLocal: endLocal!, + endGlobal: endGlobal!, + ); + } + + /// The selected range within [documentId]'s block [blockIndex], or null when + /// that block is not part of the current selection. Used by surfaces to paint + /// their highlight. + TextRange? rangeFor(Object documentId, int blockIndex) { + final sel = _selection; + if (sel == null) return null; + final (a, b) = _ordered(sel); + final di = _orderIndex(documentId); + if (di < 0) return null; + final startDoc = _orderIndex(a.documentId); + final endDoc = _orderIndex(b.documentId); + if (di < startDoc || di > endDoc) return null; + final model = _docs[di].model; // di is already resolved above + if (blockIndex < 0 || blockIndex >= model.blocks.length) return null; + final len = markdownBlockRenderedText(model.blocks[blockIndex]).length; + final startBlock = di == startDoc ? a.blockIndex : 0; + final endBlock = di == endDoc ? b.blockIndex : model.blocks.length - 1; + if (blockIndex < startBlock || blockIndex > endBlock) return null; + final from = (di == startDoc && blockIndex == a.blockIndex) ? a.offset : 0; + final to = (di == endDoc && blockIndex == b.blockIndex) ? b.offset : len; + return TextRange(start: from.clamp(0, len), end: to.clamp(0, len)); + } + + // --- extraction ---------------------------------------------------------- + + /// The structured selected content, assembled from the models in reading + /// order (works regardless of which surfaces are mounted). + MarkdownSelectedContent selectedContent() { + final sel = _selection; + if (sel == null) { + return const MarkdownSelectedContent( + documents: []); + } + final (a, b) = _ordered(sel); + final startDoc = _orderIndex(a.documentId); + final endDoc = _orderIndex(b.documentId); + if (startDoc < 0 || endDoc < 0) { + return const MarkdownSelectedContent( + documents: []); + } + final out = []; + for (var d = startDoc; d <= endDoc; d++) { + final entry = _docs[d]; + final blocks = entry.model.blocks; + final fromBlock = d == startDoc ? a.blockIndex : 0; + final toBlock = d == endDoc ? b.blockIndex : blocks.length - 1; + final segs = []; + for (var bi = fromBlock; bi <= toBlock && bi < blocks.length; bi++) { + if (bi < 0) continue; + final text = markdownBlockRenderedText(blocks[bi]); + final from = (d == startDoc && bi == a.blockIndex) + ? a.offset.clamp(0, text.length) + : 0; + final to = (d == endDoc && bi == b.blockIndex) + ? b.offset.clamp(0, text.length) + : text.length; + if (to <= from) continue; + segs.add(MarkdownSelectedBlock( + blockIndex: bi, + type: blocks[bi].type, + text: text.substring(from, to), + renderedRange: TextRange(start: from, end: to), + block: blocks[bi], + )); + } + if (segs.isNotEmpty) { + out.add(MarkdownSelectedDocument(documentId: entry.id, blocks: segs)); + } + } + return MarkdownSelectedContent(documents: out); + } + + /// The selected text, formatted with [formatter] (or the default when null). + String getText([MarkdownSelectionFormatter? formatter]) => + (formatter ?? _formatter).format(selectedContent()); + + // --- ordering helpers ---------------------------------------------------- + + int _orderIndex(Object id) => _indexById[id] ?? -1; + + Markdown _modelOf(Object id) => _docs[_orderIndex(id)].model; + + int _compare(MarkdownPosition a, MarkdownPosition b) { + final ai = _orderIndex(a.documentId), bi = _orderIndex(b.documentId); + if (ai != bi) return ai.compareTo(bi); + if (a.blockIndex != b.blockIndex) + return a.blockIndex.compareTo(b.blockIndex); + return a.offset.compareTo(b.offset); + } + + (MarkdownPosition, MarkdownPosition) _ordered(MarkdownSelection sel) => + _compare(sel.base, sel.extent) <= 0 + ? (sel.base, sel.extent) + : (sel.extent, sel.base); + + // --- position stepping (keyboard) ---------------------------------------- + + String _blockTextAt(int docIndex, int blockIndex) { + final blocks = _docs[docIndex].model.blocks; + if (blockIndex < 0 || blockIndex >= blocks.length) return ''; + return markdownBlockRenderedText(blocks[blockIndex]); + } + + static bool _isSpace(String ch) => ch == ' ' || ch == '\t' || ch == '\n'; + + /// The next/previous block (scanning across documents) that has non-empty + /// rendered text, as `(docIndex, blockIndex)`, or null at the ends. + (int, int)? _adjacentBlock(int di, int bi, {required bool forward}) { + var d = di; + var b = bi; + // Each iteration steps b by one (rolling over document boundaries) and + // returns null once it walks off either end, so the scan always terminates. + while (true) { + if (forward) { + b++; + while (d < _docs.length && b >= _docs[d].model.blocks.length) { + d++; + b = 0; + } + if (d >= _docs.length) return null; + } else { + b--; + while (d >= 0 && b < 0) { + d--; + if (d >= 0) b = _docs[d].model.blocks.length - 1; + } + if (d < 0) return null; + } + if (_blockTextAt(d, b).isNotEmpty) return (d, b); + } + } + + MarkdownPosition _stepCharacter(MarkdownPosition p, {required bool forward}) { + final di = _orderIndex(p.documentId); + if (di < 0) return p; + final len = _blockTextAt(di, p.blockIndex).length; + if (forward) { + if (p.offset < len) return p.copyWith(offset: p.offset + 1); + final adj = _adjacentBlock(di, p.blockIndex, forward: true); + return adj == null + ? p + : MarkdownPosition( + documentId: _docs[adj.$1].id, blockIndex: adj.$2, offset: 0); + } + if (p.offset > 0) return p.copyWith(offset: p.offset - 1); + final adj = _adjacentBlock(di, p.blockIndex, forward: false); + return adj == null + ? p + : MarkdownPosition( + documentId: _docs[adj.$1].id, + blockIndex: adj.$2, + offset: _blockTextAt(adj.$1, adj.$2).length); + } + + // Limitation (v1): stepping indexes the block's rendered text by UTF-16 code + // unit, so keyboard word navigation may land inside a surrogate pair (e.g. an + // emoji or other non-BMP character). Accepted for v1. + MarkdownPosition _stepWord(MarkdownPosition p, {required bool forward}) { + final di = _orderIndex(p.documentId); + if (di < 0) return p; + final text = _blockTextAt(di, p.blockIndex); + if (forward) { + if (p.offset >= text.length) return _stepCharacter(p, forward: true); + var i = p.offset; + while (i < text.length && _isSpace(text[i])) i++; + while (i < text.length && !_isSpace(text[i])) i++; + return p.copyWith(offset: i); + } + if (p.offset <= 0) return _stepCharacter(p, forward: false); + var i = p.offset; + while (i > 0 && _isSpace(text[i - 1])) i--; + while (i > 0 && !_isSpace(text[i - 1])) i--; + return p.copyWith(offset: i); + } + + MarkdownPosition _stepLineBreak(MarkdownPosition p, {required bool forward}) { + final di = _orderIndex(p.documentId); + if (di < 0) return p; + return p.copyWith( + offset: forward ? _blockTextAt(di, p.blockIndex).length : 0); + } + + MarkdownPosition _documentBoundary({required bool forward}) { + if (forward) { + final di = _docs.length - 1; + final bi = _docs[di].model.blocks.length - 1; + return MarkdownPosition( + documentId: _docs[di].id, + blockIndex: bi < 0 ? 0 : bi, + offset: bi < 0 ? 0 : _blockTextAt(di, bi).length); + } + return MarkdownPosition( + documentId: _docs.first.id, blockIndex: 0, offset: 0); + } +} + +/// Coordinates several controllers (and external selectables) so that at most +/// one has an active selection at a time. Pass the same group +/// to each controller; when one starts a (non-collapsed) selection the others +/// are cleared. Call [clearExternal] when a non-Markdown selectable (e.g. a +/// plain `SelectableText` / `SelectionArea`) begins its own selection. +class MarkdownSelectionGroup { + final Set _members = + {}; + + void _add(MarkdownSelectionController controller) => _members.add(controller); + + void _remove(MarkdownSelectionController controller) => + _members.remove(controller); + + void _claim(MarkdownSelectionController owner) { + for (final member in _members) { + if (!identical(member, owner)) member.clear(); + } + } + + /// Clears the selection of every member controller in this group. + void clearExternal() { + for (final member in _members) member.clear(); + } +} diff --git a/lib/src/selection_scope.dart b/lib/src/selection_scope.dart new file mode 100644 index 0000000..5ab2a5d --- /dev/null +++ b/lib/src/selection_scope.dart @@ -0,0 +1,706 @@ +import 'package:flutter/cupertino.dart' + show + cupertinoTextSelectionHandleControls, + cupertinoDesktopTextSelectionHandleControls; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; + +import 'selection.dart'; + +/// Signature for building the selection context menu (toolbar), mirroring +/// `SelectableRegion.contextMenuBuilder`. +/// +/// Read [MarkdownSelectionScopeState.contextMenuButtonItems] / +/// [MarkdownSelectionScopeState.contextMenuAnchors] to build an adaptive menu, +/// or call [MarkdownSelectionScopeState.copySelection] / `selectAll` / +/// `clearSelection` from a fully custom menu. +typedef MarkdownSelectionContextMenuBuilder = Widget Function( + BuildContext context, + MarkdownSelectionScopeState state, +); + +class _ScopeMarker extends InheritedWidget { + const _ScopeMarker({ + required this.controller, + required this.state, + required super.child, + }); + + final MarkdownSelectionController controller; + final MarkdownSelectionScopeState state; + + @override + bool updateShouldNotify(_ScopeMarker old) => + !identical(controller, old.controller) || !identical(state, old.state); +} + +/// Owns Markdown selection gestures, keyboard shortcuts and the selection +/// toolbar for its subtree, and exposes the ambient +/// [MarkdownSelectionController] to descendant `MarkdownWidget`s. +/// +/// Modeled on `SelectionArea`/`SelectableRegion`: +/// +/// * A mouse/trackpad/stylus drag selects; on touch a long-press-then-drag +/// selects (so a plain swipe still scrolls an enclosing list). +/// * Keyboard shortcuts work when the scope is focused โ€” `Ctrl/Cmd+C` copies, +/// `Ctrl/Cmd+A` selects all, `Shift`+arrows extend the selection (by +/// character, word, line or document per the platform bindings), and `Esc` +/// clears it. The key bindings come from the ambient +/// `DefaultTextEditingShortcuts` (installed by `WidgetsApp`/`MaterialApp`). +/// * Right-click (desktop) or long-press (mobile) shows an adaptive context +/// toolbar; customize it with [contextMenuBuilder]. +/// +/// Everything is customizable in the same spirit as `SelectableText`: +/// [selectionColor], [contextMenuBuilder], [magnifierConfiguration], +/// [selectionControls], [focusNode] and [onSelectionChanged]. +class MarkdownSelectionScope extends StatefulWidget { + /// Creates a selection scope backed by [controller]. + const MarkdownSelectionScope({ + required this.controller, + required this.child, + this.focusNode, + this.enabled = true, + this.selectionColor, + this.contextMenuBuilder = defaultContextMenuBuilder, + this.magnifierConfiguration, + this.selectionControls, + this.onSelectionChanged, + super.key, + }); + + /// The controller that owns the selection for this subtree. + final MarkdownSelectionController controller; + + /// The subtree in which selection gestures apply. + final Widget child; + + /// An optional external focus node. When null the scope manages its own. + final FocusNode? focusNode; + + /// Whether selection gestures, handles and shortcuts are active. When false + /// the scope is inert (but still exposes the controller to descendants). + final bool enabled; + + /// The selection highlight color. Defaults to the ambient + /// `DefaultSelectionStyle`/`TextSelectionTheme` color. + final Color? selectionColor; + + /// Builds the context menu (toolbar). Defaults to an adaptive Copy / + /// Select-all toolbar; pass null to disable the toolbar entirely. + final MarkdownSelectionContextMenuBuilder? contextMenuBuilder; + + /// Magnifier configuration for touch selection/handle drags. Defaults to the + /// platform-adaptive magnifier. + final TextMagnifierConfiguration? magnifierConfiguration; + + /// Controls used to paint the selection handles. Defaults per platform. + final TextSelectionControls? selectionControls; + + /// Called whenever the selection changes. + final ValueChanged? onSelectionChanged; + + /// The default [contextMenuBuilder]: an [AdaptiveTextSelectionToolbar] built + /// from the scope's [MarkdownSelectionScopeState.contextMenuButtonItems]. + static Widget defaultContextMenuBuilder( + BuildContext context, + MarkdownSelectionScopeState state, + ) => + AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: state.contextMenuButtonItems, + anchors: state.contextMenuAnchors, + ); + + /// The nearest ambient controller, or null if there is no enclosing scope. + static MarkdownSelectionController? maybeOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType<_ScopeMarker>()?.controller; + + /// The nearest ambient controller. Throws if there is no enclosing scope. + static MarkdownSelectionController of(BuildContext context) => + maybeOf(context)!; + + /// The nearest ambient scope state, or null when there is no enclosing scope. + static MarkdownSelectionScopeState? stateOf(BuildContext context) => + context.dependOnInheritedWidgetOfExactType<_ScopeMarker>()?.state; + + @override + State createState() => MarkdownSelectionScopeState(); +} + +/// State for [MarkdownSelectionScope]. Public so a custom [contextMenuBuilder] +/// can drive it (copy/select-all/clear + toolbar geometry), mirroring +/// `SelectableRegionState`. +class MarkdownSelectionScopeState extends State { + final ContextMenuController _contextMenuController = ContextMenuController(); + final LayerLink _startHandleLink = LayerLink(); + final LayerLink _endHandleLink = LayerLink(); + final LayerLink _toolbarLink = LayerLink(); + SelectionOverlay? _selectionOverlay; + FocusNode? _internalFocusNode; + Offset? _lastSecondaryTapDown; + Offset? _lastDoubleTapDown; + MarkdownSelection? _lastSelection; + + // Multi-tap / granular selection state. [_granularity] is 1 = character, + // 2 = word, 3 = block; [_granularAnchor] is the ordered word/block range a + // word/block-granular drag grows from. + int _granularity = 1; + MarkdownSelection? _granularAnchor; + + FocusNode get _focusNode => + widget.focusNode ?? + (_internalFocusNode ??= FocusNode(debugLabel: 'MarkdownSelectionScope')); + + /// The controller this scope drives. + MarkdownSelectionController get controller => widget.controller; + + @override + void initState() { + super.initState(); + _lastSelection = widget.controller.selection; + widget.controller.addListener(_onControllerChanged); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _applySelectionColor(); + } + + @override + void didUpdateWidget(covariant MarkdownSelectionScope oldWidget) { + super.didUpdateWidget(oldWidget); + if (!identical(oldWidget.controller, widget.controller)) { + _clearHandles(); // drop leaders/overlay bound to the old controller + oldWidget.controller.removeListener(_onControllerChanged); + widget.controller.addListener(_onControllerChanged); + _lastSelection = widget.controller.selection; + _applySelectionColor(); + _syncOverlay(); + } + if (oldWidget.selectionColor != widget.selectionColor) { + _applySelectionColor(); + } + } + + @override + void dispose() { + widget.controller.removeListener(_onControllerChanged); + _contextMenuController.remove(); + _selectionOverlay?.dispose(); + _selectionOverlay = null; + _internalFocusNode?.dispose(); + super.dispose(); + } + + void _applySelectionColor() { + controller.selectionColor = widget.selectionColor ?? + DefaultSelectionStyle.of(context).selectionColor; + } + + void _onControllerChanged() { + final sel = controller.selection; + if (sel != _lastSelection) { + _lastSelection = sel; + widget.onSelectionChanged?.call(sel); + } + if (sel == null || sel.isCollapsed) hideToolbar(); + _syncOverlay(); + } + + // --- native handles + magnifier ------------------------------------------ + + bool get _handlesEnabled => + widget.enabled && + switch (Theme.of(context).platform) { + TargetPlatform.android || + TargetPlatform.iOS || + TargetPlatform.fuchsia => + true, + _ => false, + }; + + TextSelectionControls get _effectiveControls => + widget.selectionControls ?? + switch (Theme.of(context).platform) { + TargetPlatform.android || + TargetPlatform.fuchsia => + materialTextSelectionHandleControls, + TargetPlatform.linux || + TargetPlatform.windows => + desktopTextSelectionHandleControls, + TargetPlatform.iOS => cupertinoTextSelectionHandleControls, + TargetPlatform.macOS => cupertinoDesktopTextSelectionHandleControls, + }; + + TextMagnifierConfiguration get _effectiveMagnifier => + widget.magnifierConfiguration ?? + TextMagnifier.adaptiveMagnifierConfiguration; + + /// Syncs the handle overlay, deferring to a post-frame callback when called + /// during a build/layout/paint phase (e.g. a streaming `setState`). + void _syncOverlay() { + final phase = SchedulerBinding.instance.schedulerPhase; + if (phase == SchedulerPhase.persistentCallbacks || + phase == SchedulerPhase.midFrameMicrotasks) { + SchedulerBinding.instance.addPostFrameCallback((_) { + if (mounted) _updateHandlesAndOverlay(); + }); + } else { + _updateHandlesAndOverlay(); + } + } + + void _updateHandlesAndOverlay() { + if (!_handlesEnabled) { + _clearHandles(); + return; + } + final endpoints = controller.selectionHandleEndpoints(); + if (endpoints == null) { + _clearHandles(); + return; + } + _applyHandles(endpoints); + final box = context.findRenderObject() as RenderBox?; + if (box == null || !box.hasSize) return; + final startPoint = TextSelectionPoint( + box.globalToLocal( + Offset(endpoints.startGlobal.left, endpoints.startGlobal.bottom)), + TextDirection.ltr, + ); + final endPoint = TextSelectionPoint( + box.globalToLocal( + Offset(endpoints.endGlobal.right, endpoints.endGlobal.bottom)), + TextDirection.ltr, + ); + final overlay = _selectionOverlay; + if (overlay == null) { + if (Overlay.maybeOf(context) == null) return; // no host for handles + _selectionOverlay = SelectionOverlay( + context: context, + startHandleType: TextSelectionHandleType.left, + lineHeightAtStart: endpoints.startLocal.height, + onStartHandleDragStart: (d) => _onHandleDragStart(d, isStart: true), + onStartHandleDragUpdate: (d) => _onHandleDragUpdate(d, isStart: true), + onStartHandleDragEnd: (_) => _onHandleDragEnd(), + endHandleType: TextSelectionHandleType.right, + lineHeightAtEnd: endpoints.endLocal.height, + onEndHandleDragStart: (d) => _onHandleDragStart(d, isStart: false), + onEndHandleDragUpdate: (d) => _onHandleDragUpdate(d, isStart: false), + onEndHandleDragEnd: (_) => _onHandleDragEnd(), + selectionEndpoints: [startPoint, endPoint], + selectionControls: _effectiveControls, + selectionDelegate: null, + clipboardStatus: null, + startHandleLayerLink: _startHandleLink, + endHandleLayerLink: _endHandleLink, + toolbarLayerLink: _toolbarLink, + magnifierConfiguration: _effectiveMagnifier, + )..showHandles(); + } else { + overlay + ..startHandleType = TextSelectionHandleType.left + ..lineHeightAtStart = endpoints.startLocal.height + ..endHandleType = TextSelectionHandleType.right + ..lineHeightAtEnd = endpoints.endLocal.height + ..selectionEndpoints = [startPoint, endPoint]; + } + } + + /// Assigns the two handle leader layers to their owning surfaces (and clears + /// them everywhere else) in a single pass, so nothing repaints needlessly. + void _applyHandles(MarkdownHandleEndpoints? e) { + final startSurface = e?.startSurface; + final endSurface = e?.endSurface; + final startLocal = + e == null ? null : Offset(e.startLocal.left, e.startLocal.bottom); + final endLocal = + e == null ? null : Offset(e.endLocal.right, e.endLocal.bottom); + for (final surface in controller.mountedSurfaces) { + final isStart = identical(surface, startSurface); + final isEnd = identical(surface, endSurface); + surface.setSelectionHandleLayers( + startLink: isStart ? _startHandleLink : null, + startLocal: isStart ? startLocal : null, + endLink: isEnd ? _endHandleLink : null, + endLocal: isEnd ? endLocal : null, + ); + } + } + + void _clearHandles() { + _applyHandles(null); + _selectionOverlay?.hide(); + _selectionOverlay?.dispose(); + _selectionOverlay = null; + } + + void _onHandleDragStart(DragStartDetails d, {required bool isStart}) { + _selectionOverlay + ?.showMagnifier(_magnifierInfo(d.globalPosition, isStart: isStart)); + } + + void _onHandleDragUpdate(DragUpdateDetails d, {required bool isStart}) { + final e = controller.selectionHandleEndpoints(); + final lineHeight = + e == null ? 0.0 : (isStart ? e.startLocal.height : e.endLocal.height); + controller.moveSelectionEdgeToGlobal( + d.globalPosition - Offset(0, lineHeight / 2), + isStart: isStart, + ); + _selectionOverlay + ?.updateMagnifier(_magnifierInfo(d.globalPosition, isStart: isStart)); + } + + void _onHandleDragEnd() { + _selectionOverlay?.hideMagnifier(); + showToolbar(); + } + + MagnifierInfo _magnifierInfo(Offset gesture, {required bool isStart}) { + final e = controller.selectionHandleEndpoints(); + final caret = e == null + ? Rect.fromCenter(center: gesture, width: 0, height: 24) + : (isStart ? e.startGlobal : e.endGlobal); + final bounds = e == null + ? caret + : (isStart ? e.startSurface.globalBounds : e.endSurface.globalBounds); + return MagnifierInfo( + globalGesturePosition: gesture, + caretRect: caret, + fieldBounds: bounds, + currentLineBoundaries: bounds, + ); + } + + // --- public selection ops ------------------------------------------------ + + /// Copies the current selection to the clipboard and hides the toolbar. + Future copySelection() async { + final text = controller.getText(); + if (text.isNotEmpty) { + await Clipboard.setData(ClipboardData(text: text)); + } + hideToolbar(); + } + + /// Selects everything across the registered documents. + void selectAll() { + _focusNode.requestFocus(); + controller.selectAll(); + } + + /// Clears the selection and hides the toolbar. + void clearSelection() { + controller.clear(); + hideToolbar(); + } + + // --- context menu -------------------------------------------------------- + + /// The default toolbar buttons for the current selection: Copy (when + /// something is selected) and Select-all (when there is any content). + List get contextMenuButtonItems { + final items = []; + final sel = controller.selection; + if (sel != null && !sel.isCollapsed) { + items.add(ContextMenuButtonItem( + type: ContextMenuButtonType.copy, + onPressed: copySelection, + )); + } + if (controller.hasDocuments) { + items.add(ContextMenuButtonItem( + type: ContextMenuButtonType.selectAll, + onPressed: () { + selectAll(); + showToolbar(); + }, + )); + } + return items; + } + + /// Where to anchor the toolbar: the last right-click point, else the top / + /// bottom center of the selection's bounding box. + TextSelectionToolbarAnchors get contextMenuAnchors { + final secondary = _lastSecondaryTapDown; + if (secondary != null) { + return TextSelectionToolbarAnchors(primaryAnchor: secondary); + } + final rects = controller.globalSelectionRects(); + if (rects.isEmpty) { + final box = context.findRenderObject() as RenderBox?; + final bounds = box != null && box.hasSize + ? box.localToGlobal(Offset.zero) & box.size + : Rect.zero; + return TextSelectionToolbarAnchors( + primaryAnchor: bounds.topCenter, + secondaryAnchor: bounds.bottomCenter, + ); + } + var bounds = rects.first; + for (final rect in rects.skip(1)) { + bounds = bounds.expandToInclude(rect); + } + return TextSelectionToolbarAnchors( + primaryAnchor: bounds.topCenter, + secondaryAnchor: bounds.bottomCenter, + ); + } + + /// Whether the toolbar is currently visible. + bool get toolbarIsVisible => _contextMenuController.isShown; + + /// Shows the context toolbar. [location] anchors it at a point (e.g. the + /// right-click position); otherwise it anchors to the selection. + void showToolbar([Offset? location]) { + final builder = widget.contextMenuBuilder; + if (builder == null) return; + if (Overlay.maybeOf(context, rootOverlay: true) == null) return; + _lastSecondaryTapDown = location; + _contextMenuController.remove(); + _contextMenuController.show( + context: context, + contextMenuBuilder: (context) => builder(context, this), + ); + } + + /// Hides the context toolbar. + void hideToolbar() { + _lastSecondaryTapDown = null; + _contextMenuController.remove(); + } + + // --- gestures ------------------------------------------------------------ + + /// Anchors a fresh selection at [global] with the given consecutive + /// [tapCount]: 1 collapses a caret (character granularity), 2 selects the + /// word, 3+ selects the whole block. Stores the granular anchor for a drag. + void _beginSelection(Offset global, int tapCount) { + _granularity = tapCount <= 1 ? 1 : (tapCount == 2 ? 2 : 3); + switch (_granularity) { + case 2: + _granularAnchor = controller.selectWordAtGlobal(global); + case 3: + _granularAnchor = controller.selectBlockAtGlobal(global); + default: + controller.startAtGlobal(global); + _granularAnchor = null; + } + } + + /// Extends the active selection to [global] at the current granularity. + void _updateSelection(Offset global) { + final anchor = _granularAnchor; + if (_granularity == 1 || anchor == null) { + controller.extendToGlobal(global); + } else { + controller.extendSelectionGranular(anchor, global, + word: _granularity == 2); + } + } + + /// Whether a Shift-click should extend (rather than replace) the selection. + bool get _shiftHeld => HardwareKeyboard.instance.isShiftPressed; + + void _handleTapDown(TapDragDownDetails d) { + _focusNode.requestFocus(); + hideToolbar(); + } + + void _handleTapUp(TapDragUpDetails d) { + // Shift-click extends the existing selection to the tapped point. + if (_shiftHeld && + d.consecutiveTapCount <= 1 && + controller.selection != null) { + _granularity = 1; + _granularAnchor = null; + controller.extendToGlobal(d.globalPosition); + return; + } + _beginSelection(d.globalPosition, d.consecutiveTapCount); + } + + void _handleDragStart(TapDragStartDetails d) { + _focusNode.requestFocus(); + hideToolbar(); + if (_shiftHeld && + d.consecutiveTapCount <= 1 && + controller.selection != null) { + _granularity = 1; + _granularAnchor = null; + controller.extendToGlobal(d.globalPosition); + return; + } + _beginSelection(d.globalPosition, d.consecutiveTapCount); + } + + Map get _gestures => + { + // Mouse / stylus / trackpad: taps (single clears, double selects a + // word, triple a block) and drags, with word/block-granular drags and + // Shift-click extension โ€” all from one recognizer so consecutive-tap + // counting stays intact. + TapAndPanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapAndPanGestureRecognizer( + supportedDevices: const { + PointerDeviceKind.mouse, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + PointerDeviceKind.trackpad, + }, + ), + (recognizer) => recognizer + ..dragStartBehavior = DragStartBehavior.down + ..onTapDown = _handleTapDown + ..onTapUp = _handleTapUp + ..onDragStart = _handleDragStart + ..onDragUpdate = ((d) => _updateSelection(d.globalPosition)), + ), + // Touch: a double-tap selects the word under the finger and pops the + // toolbar (the familiar mobile gesture). Tap-based, so it never steals + // a scroll drag from an enclosing list. + DoubleTapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => DoubleTapGestureRecognizer( + supportedDevices: const { + PointerDeviceKind.touch + }, + ), + (recognizer) => recognizer + ..onDoubleTapDown = ((d) => _lastDoubleTapDown = d.globalPosition) + ..onDoubleTap = (() { + final pos = _lastDoubleTapDown; + if (pos == null) return; + _focusNode.requestFocus(); + controller.selectWordAtGlobal(pos); + showToolbar(); + }), + ), + // Touch: long-press selects the word under the finger, then drags + // extend by word (a plain swipe still scrolls an enclosing list). + LongPressGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(), + (recognizer) => recognizer + ..onLongPressStart = ((d) { + _focusNode.requestFocus(); + hideToolbar(); + _beginSelection(d.globalPosition, 2); + }) + ..onLongPressMoveUpdate = + ((d) => _updateSelection(d.globalPosition)) + ..onLongPressEnd = ((_) => showToolbar()), + ), + // Secondary-only: right-click opens the context toolbar. With no + // primary callbacks this recognizer ignores primary taps, so it never + // competes with the TapAndPan recognizer above. + TapGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(), + (recognizer) => recognizer + ..onSecondaryTapDown = + ((d) => _lastSecondaryTapDown = d.globalPosition) + ..onSecondaryTapUp = ((d) { + _focusNode.requestFocus(); + showToolbar(d.globalPosition); + }), + ), + }; + + late final Map> _actions = >{ + CopySelectionTextIntent: CallbackAction( + onInvoke: (_) { + copySelection(); + return null; + }, + ), + SelectAllTextIntent: CallbackAction( + onInvoke: (_) { + selectAll(); + return null; + }, + ), + ExtendSelectionByCharacterIntent: + CallbackAction( + onInvoke: (intent) { + if (!intent.collapseSelection) { + controller.extendSelectionByCharacter(forward: intent.forward); + } + return null; + }, + ), + ExtendSelectionToNextWordBoundaryIntent: + CallbackAction( + onInvoke: (intent) { + if (!intent.collapseSelection) { + controller.extendSelectionByWord(forward: intent.forward); + } + return null; + }, + ), + ExtendSelectionToLineBreakIntent: + CallbackAction( + onInvoke: (intent) { + if (!intent.collapseSelection) { + controller.extendSelectionToLineBreak(forward: intent.forward); + } + return null; + }, + ), + ExtendSelectionVerticallyToAdjacentLineIntent: + CallbackAction( + onInvoke: (intent) { + if (!intent.collapseSelection) { + controller.extendSelectionToAdjacentLine(forward: intent.forward); + } + return null; + }, + ), + ExtendSelectionToDocumentBoundaryIntent: + CallbackAction( + onInvoke: (intent) { + if (!intent.collapseSelection) { + controller.extendSelectionToDocumentBoundary(forward: intent.forward); + } + return null; + }, + ), + DismissIntent: CallbackAction( + onInvoke: (_) { + clearSelection(); + return null; + }, + ), + }; + + @override + Widget build(BuildContext context) { + if (!widget.enabled) { + return _ScopeMarker( + controller: widget.controller, + state: this, + child: widget.child, + ); + } + return _ScopeMarker( + controller: widget.controller, + state: this, + child: Actions( + actions: _actions, + child: Focus( + focusNode: _focusNode, + child: RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: _gestures, + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/src/theme.dart b/lib/src/theme.dart index 60be404..bca6016 100644 --- a/lib/src/theme.dart +++ b/lib/src/theme.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import '../flutter_md.dart'; +import 'highlight/engine.dart'; /// {@template markdown_theme_data} /// Theme data for Markdown widgets. @@ -32,6 +33,7 @@ class MarkdownThemeData implements ThemeExtension { this.spanFilter, this.builder, this.onLinkTap, + this.highlighter, }) : _headingStyles = List.filled(8, null), _textStyles = HashMap(); @@ -59,6 +61,7 @@ class MarkdownThemeData implements ThemeExtension { bool Function(MD$Span span)? spanFilter, BlockPainter? Function(MD$Block block, MarkdownThemeData theme)? builder, void Function(String title, String url)? onLinkTap, + SyntaxHighlighter? highlighter, }) { return MarkdownThemeData( textStyle: textStyle ?? @@ -89,6 +92,7 @@ class MarkdownThemeData implements ThemeExtension { spanFilter: spanFilter, builder: builder, onLinkTap: onLinkTap, + highlighter: highlighter, ); } @@ -193,6 +197,13 @@ class MarkdownThemeData implements ThemeExtension { /// It receives the link title and URL as parameters. final void Function(String title, String url)? onLinkTap; + /// An optional syntax highlighter for fenced code blocks. When `null`, code + /// is painted as plain monospace text. + /// + /// See `package:flutter_md/highlight.dart` and [MarkdownHighlighter]. Assign + /// the exact set of languages you support so unused grammars tree-shake away. + final SyntaxHighlighter? highlighter; + final List _headingStyles; /// Returns a [TextStyle] for the given heading level. @@ -300,6 +311,7 @@ class MarkdownThemeData implements ThemeExtension { bool Function(MD$Span span)? spanFilter, BlockPainter? Function(MD$Block block, MarkdownThemeData theme)? builder, void Function(String title, String url)? onLinkTap, + SyntaxHighlighter? highlighter, }) => MarkdownThemeData( textDirection: textDirection ?? this.textDirection, @@ -325,6 +337,7 @@ class MarkdownThemeData implements ThemeExtension { spanFilter: spanFilter ?? this.spanFilter, builder: builder ?? this.builder, onLinkTap: onLinkTap ?? this.onLinkTap, + highlighter: highlighter ?? this.highlighter, ); @override @@ -360,6 +373,7 @@ class MarkdownThemeData implements ThemeExtension { spanFilter: t < 0.5 ? spanFilter : other?.spanFilter, builder: t < 0.5 ? builder : other?.builder, onLinkTap: t < 0.5 ? onLinkTap : other?.onLinkTap, + highlighter: t < 0.5 ? highlighter : other?.highlighter, ); } diff --git a/lib/src/widget.dart b/lib/src/widget.dart index 1bb6dcc..1d65e8a 100644 --- a/lib/src/widget.dart +++ b/lib/src/widget.dart @@ -2,6 +2,8 @@ import 'package:flutter/widgets.dart'; import 'markdown.dart' show Markdown; import 'render.dart' show MarkdownRenderObject; +import 'selection.dart' show MarkdownSelectionController; +import 'selection_scope.dart' show MarkdownSelectionScope; import 'theme.dart'; /// {@template markdown_widget} @@ -12,6 +14,8 @@ class MarkdownWidget extends LeafRenderObjectWidget { const MarkdownWidget({ required this.markdown, this.theme, + this.controller, + this.documentId, super.key, // ignore: unused_element }); @@ -21,38 +25,43 @@ class MarkdownWidget extends LeafRenderObjectWidget { /// Current theme for the markdown widget. final MarkdownThemeData? theme; + /// The selection controller this widget participates in. When null, the + /// nearest [MarkdownSelectionScope] controller is used, if any. + final MarkdownSelectionController? controller; + + /// The stable document id used to anchor selection positions. Selection is + /// only enabled when this is non-null AND a controller is available; the app + /// must register this document's model with the controller. + final Object? documentId; + + MarkdownThemeData _resolveTheme(BuildContext context) => + theme ?? + MarkdownTheme.maybeOf(context) ?? + MarkdownThemeData( + textStyle: DefaultTextStyle.of(context).style, + textDirection: Directionality.maybeOf(context) ?? TextDirection.ltr, + textScaler: + MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling, + ); + + MarkdownSelectionController? _resolveController(BuildContext context) => + documentId == null + ? null + : (controller ?? MarkdownSelectionScope.maybeOf(context)); + @override - RenderObject createRenderObject(BuildContext context) { - final theme = this.theme ?? - MarkdownTheme.maybeOf(context) ?? - MarkdownThemeData( - textStyle: DefaultTextStyle.of(context).style, - textDirection: Directionality.maybeOf(context) ?? TextDirection.ltr, - textScaler: - MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling, - ); - return MarkdownRenderObject( - markdown: markdown, - theme: theme, - ); - } + RenderObject createRenderObject(BuildContext context) => MarkdownRenderObject( + markdown: markdown, + theme: _resolveTheme(context), + )..updateSelection(_resolveController(context), documentId); @override void updateRenderObject( BuildContext context, MarkdownRenderObject renderObject, ) { - final theme = this.theme ?? - MarkdownTheme.maybeOf(context) ?? - MarkdownThemeData( - textStyle: DefaultTextStyle.of(context).style, - textDirection: Directionality.maybeOf(context) ?? TextDirection.ltr, - textScaler: - MediaQuery.maybeTextScalerOf(context) ?? TextScaler.noScaling, - ); - renderObject.update( - markdown: markdown, - theme: theme, - ); + renderObject + ..update(markdown: markdown, theme: _resolveTheme(context)) + ..updateSelection(_resolveController(context), documentId); } } diff --git a/pubspec.yaml b/pubspec.yaml index fb86420..666a301 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ description: > Markdown library written in Dart. It can parse and display Markdown. -version: 0.1.0 +version: 0.2.0 homepage: https://github.com/DoctorinaAI/md repository: https://github.com/DoctorinaAI/md diff --git a/test/highlight/highlight_test.dart b/test/highlight/highlight_test.dart new file mode 100644 index 0000000..821f9ae --- /dev/null +++ b/test/highlight/highlight_test.dart @@ -0,0 +1,446 @@ +import 'dart:math'; + +import 'package:flutter/painting.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_md/highlight.dart'; +import 'package:flutter_md/highlight/all.dart'; +import 'package:flutter_md/highlight/themes.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Flattens a span tree into (text, effectiveStyle) fragments, resolving the +/// inherited style down the tree exactly as the text engine would. +List<(String, TextStyle)> _fragments(InlineSpan span, [TextStyle? inherited]) { + final out = <(String, TextStyle)>[]; + void walk(InlineSpan node, TextStyle acc) { + if (node is! TextSpan) return; + final merged = node.style == null ? acc : acc.merge(node.style); + final text = node.text; + if (text != null && text.isNotEmpty) out.add((text, merged)); + for (final child in node.children ?? const []) { + walk(child, merged); + } + } + + walk(span, inherited ?? const TextStyle()); + return out; +} + +/// The effective color of the first fragment whose text equals [needle]. +Color? _colorOf(List<(String, TextStyle)> frags, String needle) { + for (final (text, style) in frags) { + if (text == needle) return style.color; + } + return null; +} + +void main() { + const base = TextStyle(fontFamily: 'monospace', fontSize: 14); + + MarkdownHighlighter highlighterWith(Map langs) => + MarkdownHighlighter(languages: langs, theme: HighlightThemes.githubDark); + + // Tokenizes [src] under the grammar registered for [tag] and returns the + // concatenated span text (which must equal [src] for a lossless highlighter). + String roundTrip(String tag, String src) { + final grammar = allHighlightLanguages[tag]!; + final highlighter = highlighterWith({tag: grammar}); + return TextSpan(children: highlighter.highlight(src, tag, base)) + .toPlainText(); + } + + group('Highlight โ€บ losslessness (selection stays aligned)', () { + final cases = { + 'dart': ( + HighlightDart.grammar, + '// a comment\n' + 'class Foo extends Bar {\n' + ' final String name = "hi \$world and \${a.b}";\n' + ' int n = 0x1F + 42; // trailing\n' + ' @override void f() => print(r\'raw\');\n' + '}\n', + ), + 'json': ( + HighlightJson.grammar, + '{"a": 1, "b": "x", "c": [true, null], "d": 3.14e2}', + ), + 'bash': ( + HighlightBash.grammar, + '#!/usr/bin/env bash\n' + '# comment\n' + 'name="world"\n' + 'echo "hello \$name \${name:-def} \$(date)"\n' + 'for i in 1 2 3; do echo "\$i"; done\n', + ), + }; + + for (final MapEntry(key: lang, value: (grammar, source)) in cases.entries) { + test('$lang round-trips to identical text', () { + final highlighter = highlighterWith({lang: grammar}); + final spans = highlighter.highlight(source, lang, base); + final joined = TextSpan(children: spans).toPlainText(); + expect(joined, source, reason: 'highlighting must not alter text'); + }); + } + }); + + group('Highlight โ€บ tokens are colored', () { + test('dart keyword / comment / number use the theme palette', () { + final highlighter = highlighterWith({'dart': HighlightDart.grammar}); + const code = '// hi\nclass Foo {}\nvar n = 42;'; + final frags = _fragments(TextSpan( + style: base, + children: highlighter.highlight(code, 'dart', base), + )); + + expect(_colorOf(frags, 'class'), const Color(0xFFFF7B72)); // keyword + expect(_colorOf(frags, '42'), const Color(0xFF79C0FF)); // number + + final comment = frags.firstWhere((f) => f.$1.startsWith('//')); + expect(comment.$2.color, const Color(0xFF8B949E)); + expect(comment.$2.fontStyle, FontStyle.italic); + }); + + test('python keyword / string / comment use the theme palette', () { + final highlighter = MarkdownHighlighter( + languages: {'python': HighlightPython.grammar}, + theme: HighlightThemes.githubDark); + const code = '# c\ndef greet(name):\n return "hi"'; + final frags = _fragments(TextSpan( + style: base, + children: highlighter.highlight(code, 'python', base), + )); + expect(_colorOf(frags, 'def'), const Color(0xFFFF7B72)); // keyword + expect(_colorOf(frags, '"hi"'), const Color(0xFFA5D6FF)); // string + final comment = frags.firstWhere((f) => f.$1.startsWith('#')); + expect(comment.$2.color, const Color(0xFF8B949E)); + }); + + test('json string vs number are distinguished', () { + final highlighter = highlighterWith({'json': HighlightJson.grammar}); + const code = '{"k": "v", "n": 7}'; + final frags = _fragments(TextSpan( + style: base, + children: highlighter.highlight(code, 'json', base), + )); + expect(_colorOf(frags, '"v"'), const Color(0xFFA5D6FF)); // string + expect(_colorOf(frags, '7'), const Color(0xFF79C0FF)); // number + }); + }); + + group('Highlight โ€บ all bundled grammars', () { + // A varied polyglot blob: tags, comments, interpolated strings, numbers, + // template markers, keywords. Tokenizing it under every grammar exercises + // regex compilation, nested/rest recursion, and lossless partitioning. + const sample = r''' +link +/* block comment */ // line comment +const greeting = "hi $name and ${a.b}"; +let n = 0xFF + 42 - 3.14e2; + +name: value # yaml-ish comment +SELECT * FROM t WHERE id = 1; +def f(x): return x +'''; + + test('every grammar builds, tokenizes and round-trips losslessly', () { + final failures = []; + for (final MapEntry(key: tag, value: grammar) + in allHighlightLanguages.entries) { + final highlighter = MarkdownHighlighter( + languages: {tag: grammar}, + theme: HighlightThemes.githubDark, + ); + final joined = + TextSpan(children: highlighter.highlight(sample, tag, base)) + .toPlainText(); + if (joined != sample) failures.add(tag); + } + expect(failures, isEmpty, reason: 'text altered for: $failures'); + }); + + test('registry covers the popular languages and their aliases', () { + // Spot-check that common fence tags resolve. + for (final tag in const [ + 'js', + 'javascript', + 'ts', + 'typescript', + 'py', + 'python', + 'rb', + 'ruby', + 'html', + 'css', + 'scss', + 'go', + 'rust', + 'java', + 'kotlin', + 'swift', + 'c', + 'cpp', + 'csharp', + 'php', + 'sql', + 'yaml', + 'json', + 'bash', + 'sh', + ]) { + expect(allHighlightLanguages.containsKey(tag), isTrue, + reason: 'missing $tag'); + } + }); + }); + + group('Highlight โ€บ fallbacks', () { + test('unknown language yields a single plain span equal to the source', () { + final highlighter = highlighterWith({'dart': HighlightDart.grammar}); + const code = 'nothing to see here'; + final spans = highlighter.highlight(code, 'ruby', base); + expect(spans, hasLength(1)); + expect(TextSpan(children: spans).toPlainText(), code); + }); + + test('null language yields plain text', () { + final highlighter = highlighterWith({'dart': HighlightDart.grammar}); + final spans = highlighter.highlight('x = 1', null, base); + expect(TextSpan(children: spans).toPlainText(), 'x = 1'); + }); + + test('language tag is matched case-insensitively', () { + final highlighter = highlighterWith({'dart': HighlightDart.grammar}); + final frags = _fragments(TextSpan( + style: base, + children: highlighter.highlight('class X {}', 'DART', base), + )); + expect(_colorOf(frags, 'class'), const Color(0xFFFF7B72)); + }); + }); + + group('Highlight โ€บ BlockPainter\$Code integration', () { + test('renderedText equals source (selection offset space preserved)', () { + final theme = MarkdownThemeData( + textStyle: base, + highlighter: highlighterWith({'dart': HighlightDart.grammar}), + ); + const code = 'void main() => print("hi"); // c'; + final painter = BlockPainter$Code( + text: code, + language: 'dart', + theme: theme, + ); + expect(painter.renderedText, code); + painter.dispose(); + }); + + test('no highlighter keeps plain-text behavior', () { + final theme = MarkdownThemeData(textStyle: base); + const code = 'class Foo {}'; + final painter = BlockPainter$Code( + text: code, + language: 'dart', + theme: theme, + ); + expect(painter.renderedText, code); + painter.dispose(); + }); + + test('delegates to the theme highlighter for the block language', () { + final spy = _SpyHighlighter(); + final theme = MarkdownThemeData(textStyle: base, highlighter: spy); + final painter = + BlockPainter$Code(text: 'x = 1', language: 'dart', theme: theme); + expect(spy.seenLanguages, contains('dart')); + expect(painter.renderedText, 'x = 1'); + painter.dispose(); + }); + }); + + group('Highlight โ€บ corner cases', () { + test('empty and whitespace-only inputs round-trip and do not throw', () { + for (final src in const ['', ' ', '\n', '\n\n', '\t \n ', ' ']) { + for (final tag in const ['dart', 'js', 'html', 'json', 'bash']) { + expect(roundTrip(tag, src), src, reason: 'tag=$tag'); + } + } + }); + + test('empty code yields an empty (or plain) span with no text', () { + final highlighter = highlighterWith({'dart': HighlightDart.grammar}); + final joined = TextSpan(children: highlighter.highlight('', 'dart', base)) + .toPlainText(); + expect(joined, isEmpty); + }); + + test('unicode, emoji (surrogate pairs) and CJK are preserved', () { + const src = '// ฯ€ โ‰ˆ 3.14 ไฝ ๅฅฝ, ไธ–็•Œ ๐Ÿ‘๐Ÿฝ ๅฎถๆ—\n' + 'const s = "cafรฉ โ€” naรฏve โ€” ๐Ÿš€"; // โœ…'; + for (final tag in const ['dart', 'python', 'js', 'json', 'yaml']) { + expect(roundTrip(tag, src), src, reason: 'tag=$tag'); + } + }); + + test('CRLF and mixed line endings are preserved verbatim', () { + const src = 'a = 1\r\n// c\r\nb = 2\rlast'; + for (final tag in const ['dart', 'js', 'bash', 'sql']) { + expect(roundTrip(tag, src), src, reason: 'tag=$tag'); + } + }); + + test('deeply nested interpolation does not overflow and round-trips', () { + final buffer = StringBuffer('"'); + for (var i = 0; i < 200; i++) { + buffer.write(r'${x + '); + } + buffer.write('1'); + for (var i = 0; i < 200; i++) { + buffer.write('}'); + } + buffer.write('"'); + final src = buffer.toString(); + expect(roundTrip('dart', src), src); + }); + + test('templating language with a rest grammar (php+html) round-trips', () { + const src = '
\n' + ' \n' + '
'; + expect(roundTrip('php', src), src); + }); + + test('a very long input with many tokens round-trips', () { + final src = ('final x = "s"; // comment ${'ab12 ' * 4}\n' * 500); + expect(roundTrip('dart', src), src); + }); + + test('custom grammar exercises lookbehind, nested inside and rest', () { + final inner = + Grammar([GrammarToken('number', compileHighlightPattern(r'\d+'))]); + final rest = + Grammar([GrammarToken('keyword', compileHighlightPattern('foo'))]); + final grammar = Grammar( + [ + GrammarToken('comment', compileHighlightPattern('//.*')), + // Lookbehind: group 1 ('@') is excluded from the emitted token. + GrammarToken('function', compileHighlightPattern(r'(@)\w+'), + lookbehind: true), + GrammarToken('string', compileHighlightPattern(r'\{[^}]*\}'), + inside: () => inner), + ], + rest: () => rest, + ); + final highlighter = highlighterWith({'x': grammar}); + const src = '// c\n@name {42} foo'; + final spans = highlighter.highlight(src, 'x', base); + final frags = _fragments(TextSpan(style: base, children: spans)); + + expect(TextSpan(children: spans).toPlainText(), src); + expect(_colorOf(frags, 'name'), const Color(0xFFD2A8FF)); // function + // Lookbehind excluded '@', so the token is 'name', never '@name'. + expect(_colorOf(frags, '@name'), isNull); + expect(_colorOf(frags, '42'), const Color(0xFF79C0FF)); // inside โ†’ number + expect(_colorOf(frags, 'foo'), const Color(0xFFFF7B72)); // rest โ†’ keyword + }); + + test('compileHighlightPattern: invalid source disables the rule', () { + final bad = compileHighlightPattern('(unclosed'); + expect(bad.hasMatch('an (unclosed group'), isFalse); + final good = compileHighlightPattern('abc', caseSensitive: false); + expect(good.hasMatch('xxABCxx'), isTrue); + }); + + test('a rule whose regex Dart rejects is skipped, text still round-trips', + () { + final grammar = Grammar([ + GrammarToken( + 'bad', compileHighlightPattern(r'(?<= )\K')), // unsupported โ†’ never + GrammarToken('number', compileHighlightPattern(r'\d+')), + ]); + final highlighter = highlighterWith({'x': grammar}); + const src = 'value 42 end'; + expect( + TextSpan(children: highlighter.highlight(src, 'x', base)).toPlainText(), + src, + ); + }); + + test('empty languages map falls back to plain text for any tag', () { + final highlighter = MarkdownHighlighter( + languages: const {}, theme: HighlightThemes.githubDark); + const code = 'class Foo {}'; + final spans = highlighter.highlight(code, 'dart', base); + expect(spans, hasLength(1)); + expect(TextSpan(children: spans).toPlainText(), code); + }); + + test('styleFor returns null for an unknown token type', () { + expect(HighlightThemes.githubDark.styleFor('no-such-token'), isNull); + expect( + HighlightThemes.githubLight.styleFor('definitely-unknown'), isNull); + }); + + test('theme background/foreground surface through the highlighter', () { + final highlighter = highlighterWith({'dart': HighlightDart.grammar}); + expect(highlighter.backgroundFor('dart'), const Color(0xFF0D1117)); + final derived = highlighter.baseStyleFor('dart', base); + expect(derived.color, const Color(0xFFC9D1D9)); + expect(derived.fontFamily, 'monospace'); // fallback preserved + }); + + test('deterministic fuzz: random sources round-trip under many grammars', + () { + final rng = Random(20240805); + // Chars that stress grammars: quotes, braces, comment/interp markers, + // escapes, newlines, tabs. + const alphabet = "abc 09{}()[]<>\"'/*#\$\\\n\t.:;=|&-_`~"; + const tags = [ + 'dart', + 'js', + 'ts', + 'html', + 'php', + 'python', + 'ruby', + 'bash', + 'json', + 'yaml', + 'sql', + 'css', + 'go', + 'rust', + 'markdown', + ]; + for (var i = 0; i < 300; i++) { + final len = rng.nextInt(96); + final sb = StringBuffer(); + for (var j = 0; j < len; j++) { + sb.write(alphabet[rng.nextInt(alphabet.length)]); + } + final src = sb.toString(); + for (final tag in tags) { + expect(roundTrip(tag, src), src, reason: 'tag=$tag len=$len iter=$i'); + } + } + }); + }); +} + +/// Records which languages it was asked to highlight; otherwise a pass-through +/// (lossless) highlighter, to verify [BlockPainter$Code] delegation. +class _SpyHighlighter implements SyntaxHighlighter { + final List seenLanguages = []; + + @override + List highlight(String code, String? language, TextStyle base) { + seenLanguages.add(language); + return [TextSpan(text: code)]; + } + + @override + Color? backgroundFor(String? language) => null; + + @override + TextStyle baseStyleFor(String? language, TextStyle fallback) => fallback; +} diff --git a/test/parser/inline_test.dart b/test/parser/inline_test.dart index 04abf79..ea73e3e 100644 --- a/test/parser/inline_test.dart +++ b/test/parser/inline_test.dart @@ -111,6 +111,52 @@ void main() => group('Inline parsing', () { expect(_spans('a ~~ b').every((s) => s.style.isEmpty), isTrue); expect(_spans('a == b').every((s) => s.style.isEmpty), isTrue); }); + + test('double marker with a space-flanked closer stays literal', () { + // The closing `**` is preceded by a space, so it is not + // right-flanking and cannot close: the whole run is literal. The + // inner `*` of the run must not degrade into a stray italic. + expect(_visible('a **bold ** x'), 'a **bold ** x'); + expect(_spans('a **bold ** x').every((s) => s.style.isEmpty), isTrue); + }); + + test('double marker with a closer after a soft break stays literal', + () { + // Same rule across a soft line break: the closing `**` follows a + // newline (whitespace), so it cannot close and stays literal. + expect(_visible('a **bold\n** x'), 'a **bold\n** x'); + expect( + _spans('a **bold\n** x').every((s) => s.style.isEmpty), isTrue); + }); + + test('double underline with a space-flanked closer stays literal', () { + expect(_visible('a __bold __ x'), 'a __bold __ x'); + expect(_spans('a __bold __ x').every((s) => s.style.isEmpty), isTrue); + }); + }); + + group('Double markers still emphasize when properly flanked', () { + // Regression guards: the fix for space-flanked closers must not break + // legitimate double/triple/nested emphasis. + test('triple *** is bold + italic', () { + final spans = _spans('***both***'); + expect(spans.single.text, 'both'); + expect(spans.single.style.contains(MD$Style.bold), isTrue); + expect(spans.single.style.contains(MD$Style.italic), isTrue); + }); + + test('italic nested inside bold', () { + final spans = _spans('**b *bi* b**'); + expect(_styleOf(spans, 'bi').contains(MD$Style.bold), isTrue); + expect(_styleOf(spans, 'bi').contains(MD$Style.italic), isTrue); + expect(_styleOf(spans, 'b '), MD$Style.bold); + }); + + test('bold spanning a soft line break is closed', () { + final spans = _spans('x **a\nb** y'); + expect(_styleOf(spans, 'a\nb'), MD$Style.bold); + expect(_visible('x **a\nb** y'), 'x a\nb y'); + }); }); group('Intraword underscores (snake_case)', () { diff --git a/test/parser/streaming_test.dart b/test/parser/streaming_test.dart new file mode 100644 index 0000000..b2bf590 --- /dev/null +++ b/test/parser/streaming_test.dart @@ -0,0 +1,358 @@ +import 'dart:math'; + +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Tests for [StreamingMarkdownParser]. +/// +/// The governing invariant is simple and strong: for *any* split of a document +/// into chunks, after feeding a prefix of those chunks the parser's [current] +/// must be identical โ€” block for block, span for span, offsets and all โ€” to +/// `Markdown.fromString(thatPrefix)`. The incremental path is only ever an +/// optimization, so it must never diverge from the batch decoder at any step. +/// +/// Most cases therefore run a document through many chunkings (whole, per-line, +/// per-word, fixed sizes, char-by-char, and seeded-random) and assert the +/// invariant after *every* chunk. That turns a handful of documents into +/// thousands of prefix comparisons and exercises every mid-construct boundary +/// (a fence opened but not closed, a table header before its delimiter row, a +/// blank run split across chunks, a surrogate pair split down the middle, โ€ฆ). +void main() => group('StreamingMarkdownParser', () { + // --------------------------------------------------------------------- + // The corpus: each entry is a document that exercises a specific corner. + // --------------------------------------------------------------------- + const corpus = { + 'empty': '', + 'blank-only': '\n\n\n', + 'spaces-only': ' \n \t \n', + 'single-paragraph': 'Just a single line of prose.', + 'paragraph-no-newline': 'No trailing newline here', + 'two-paragraphs': 'First paragraph.\n\nSecond paragraph.', + 'paragraphs-trailing-blank': 'P1\n\nP2\n\nP3\n\n', + 'wide-blank-run': 'A\n\n\n\n\nB', + 'leading-blanks': '\n\n\nAfter some blanks.', + 'soft-wrapped-paragraph': 'Line one\nline two\nline three.', + 'headings': '# H1\n\n## H2\n\n### H3 ###\n\ntext after', + 'heading-then-para': '# Title\nA paragraph right after.', + 'not-a-heading': '#hashtag is not a heading\n\n####### seven hashes', + 'thematic-breaks': 'a\n\n---\n\nb\n\n***\n\nc\n\n___\n\nd', + 'divider-dashes': 'text\n---\nmore', + 'quote': '> quote line one\n> quote line two\n\nafter', + 'quote-multi': '> a\n> b\n> c', + 'alert-note': '> [!NOTE]\n> Body of the note.\n\nafter', + 'alert-warning': '> [!WARNING]\n> Careful now.\n> Second line.', + 'code-closed': '```dart\nvoid main() {}\n```\n\nafter code', + 'code-tilde': '~~~\nplain\n~~~\n\nafter', + 'code-unclosed': '```dart\nline 1\nline 2\nstill going', + 'code-blank-inside': '```\n\n\ncode after blanks\n\n```\n\nafter', + 'code-then-code': '```\na\n```\n\n```\nb\n```\n\ntail', + 'code-fence-lookalike': '```\nnot ``` a real close\n```\n\nx', + 'list-unordered': '- one\n- two\n- three\n\nafter', + 'list-ordered': '1. one\n2. two\n3. three', + 'list-nested': '- a\n - a1\n - a2\n- b\n\nafter', + 'list-tasks': '- [x] done\n- [ ] todo\n- [X] also done', + 'list-then-para': '- item\n\nnot a list anymore', + 'table-full': '| A | B |\n| - | - |\n| 1 | 2 |\n| 3 | 4 |\n\nafter', + 'table-aligned': '| L | C | R |\n| :- | :-: | -: |\n| a | b | c |', + 'table-header-first': '| Col1 | Col2 |\n| ---- | ---- |\n| v1 | v2 |', + 'table-malformed': '| looks | like |\nbut no delimiter row', + 'pipes-not-table': 'a | b | c is just prose with pipes', + 'inline-styles': + 'Some **bold**, _italic_, `code`, ~~strike~~ and ==mark==.', + 'emoji-and-unicode': '# Welcome ๐Ÿ‘‹\n\nUnicode: cafรฉ, naรฏve, ๆ—ฅๆœฌ่ชž, ๐Ÿš€.', + 'crlf-doc': 'para one\r\n\r\n## Heading\r\n\r\npara two\r\n', + 'crlf-code': '```\r\ncode\r\n```\r\n\r\nafter\r\n', + 'mixed': _mixed, + }; + + // Chunkings that all reconstruct the original exactly. + List whole(String s) => [if (s.isNotEmpty) s]; + + List chars(String s) => + [for (var i = 0; i < s.length; i++) s[i]]; + + List byWord(String s) { + final parts = s.split(' '); + return [ + for (var i = 0; i < parts.length; i++) + i == 0 ? parts[i] : ' ${parts[i]}', + ]; + } + + List byLine(String s) { + final parts = s.split('\n'); + return [ + for (var i = 0; i < parts.length; i++) + i < parts.length - 1 ? '${parts[i]}\n' : parts[i], + ]; + } + + List bySize(String s, int n) => [ + for (var i = 0; i < s.length; i += n) + s.substring(i, min(i + n, s.length)), + ]; + + List byRandom(String s, int seed) { + final rng = Random(seed); + final out = []; + var i = 0; + while (i < s.length) { + final take = 1 + rng.nextInt(7); + out.add(s.substring(i, min(i + take, s.length))); + i += take; + } + return out; + } + + /// Feeds [chunks] into a fresh parser and asserts the invariant after + /// every chunk against the batch decoder. Returns the final parser so + /// callers can make extra assertions (e.g. on [stableBlockCount]). + StreamingMarkdownParser feed( + List chunks, { + bool inlineMath = false, + String reason = '', + }) { + // The chunking must reconstruct the source, or the test is meaningless. + final doc = chunks.join(); + final decoder = MarkdownDecoder(inlineMath: inlineMath); + final parser = StreamingMarkdownParser(decoder: decoder); + final acc = StringBuffer(); + for (var i = 0; i < chunks.length; i++) { + parser.add(chunks[i]); + acc.write(chunks[i]); + final expected = decoder.convert(acc.toString()); + expect( + _sig(parser.current), + _sig(expected), + reason: '$reason after chunk ${i + 1}/${chunks.length}', + ); + expect(parser.source, acc.toString(), reason: '$reason source'); + } + // Final result matches a single-shot parse of the whole document. + expect(_sig(parser.current), _sig(decoder.convert(doc)), + reason: '$reason final'); + return parser; + } + + // --------------------------------------------------------------------- + // Equivalence across the whole corpus and every chunking. + // --------------------------------------------------------------------- + corpus.forEach((name, doc) { + group(name, () { + test('whole', () => feed(whole(doc), reason: name)); + test('by-line', () => feed(byLine(doc), reason: name)); + test('by-word', () => feed(byWord(doc), reason: name)); + test('char-by-char', () => feed(chars(doc), reason: name)); + for (final n in const [2, 3, 5, 7, 13]) { + test('by-size-$n', () => feed(bySize(doc, n), reason: name)); + } + for (final seed in const [1, 42, 1337]) { + test('random-$seed', () => feed(byRandom(doc, seed), reason: name)); + } + }); + }); + + // --------------------------------------------------------------------- + // Inline math must flow through the injected decoder. + // --------------------------------------------------------------------- + group('inlineMath decoder', () { + const doc = r'Euler: $e^{i\pi} + 1 = 0$.' + '\n\n' + r'Roots: $x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$ and $H_2O$.'; + test('char-by-char matches batch(inlineMath)', () { + feed(chars(doc), inlineMath: true, reason: 'math'); + }); + test(r'literal $ preserved without inlineMath', () { + feed(chars(r'Price is $5 and $HOME is a var.'), reason: 'no-math'); + }); + }); + + // --------------------------------------------------------------------- + // Targeted: freezing behaviour (the whole point of the optimization). + // --------------------------------------------------------------------- + group('freezing', () { + test('completed blank-separated blocks are frozen', () { + final p = StreamingMarkdownParser(); + for (final t in const ['P1\n\n', 'P2\n\n', 'P3\n\n']) { + p.add(t); + } + // Nothing after the last blank run yet โ€” the trailing spacer stays + // live because more blanks could still arrive. + final beforeTail = p.stableBlockCount; + p.add('P4'); // real content proves the previous run is complete + expect(p.stableBlockCount, greaterThan(beforeTail)); + expect(p.stableBlockCount, greaterThanOrEqualTo(6), + reason: 'P1,Spacer,P2,Spacer,P3,Spacer should be frozen'); + expect(_sig(p.current), + _sig(Markdown.fromString('P1\n\nP2\n\nP3\n\nP4'))); + }); + + test('an open code fence never freezes', () { + final p = StreamingMarkdownParser(); + p.add('```dart\n'); + p.add('line 1\n'); + p.add('line 2\n'); + p.add('\n'); // a blank line *inside* the fence must not freeze + p.add('line 3\n'); + expect(p.stableBlockCount, 0, + reason: 'blocks are unstable while the fence is open'); + // Closing the fence and starting a new block freezes the code. + p.add('```\n\nAfter.'); + expect(p.stableBlockCount, greaterThan(0)); + expect( + _sig(p.current), + _sig(Markdown.fromString( + '```dart\nline 1\nline 2\n\nline 3\n```\n\nAfter.')), + ); + }); + + test('a table header does not freeze before its delimiter row', () { + // The PR-breaking case: the header line looks like a malformed table + // (โ†’ paragraph) until the delimiter row arrives. It must never freeze + // as a paragraph, or the table could never form. + final p = StreamingMarkdownParser(); + p.add('| A | B |\n'); + expect(p.stableBlockCount, 0); + p.add('| - | - |\n'); + expect(p.stableBlockCount, 0); + p.add('| 1 | 2 |\n'); + expect(p.stableBlockCount, 0); + expect(_sig(p.current), + _sig(Markdown.fromString('| A | B |\n| - | - |\n| 1 | 2 |\n'))); + final blocks = p.current.blocks; + expect(blocks.length, 1); + expect(blocks.single.type, 'table'); + }); + }); + + // --------------------------------------------------------------------- + // API surface: reset, empty adds, current/source, stream extension. + // --------------------------------------------------------------------- + group('api', () { + test('starts empty', () { + final p = StreamingMarkdownParser(); + expect(p.current.isEmpty, isTrue); + expect(p.source, isEmpty); + expect(p.stableBlockCount, 0); + }); + + test('empty chunks are no-ops', () { + final p = StreamingMarkdownParser(); + expect(p.add('').isEmpty, isTrue); + p.add('# Hi'); + final before = _sig(p.current); + expect(_sig(p.add('')), before); + expect(p.source, '# Hi'); + }); + + test('reset reuses the instance', () { + final p = StreamingMarkdownParser(); + p.add('# First doc\n\nbody\n\nmore'); + p.reset(); + expect(p.current.isEmpty, isTrue); + expect(p.source, isEmpty); + expect(p.stableBlockCount, 0); + p.add('# Second doc'); + expect(_sig(p.current), _sig(Markdown.fromString('# Second doc'))); + }); + + test('add returns the same as current', () { + final p = StreamingMarkdownParser(); + final returned = p.add('# Hello\n\nworld'); + expect(_sig(returned), _sig(p.current)); + }); + + test('Stream.toMarkdown emits growing, batch-equivalent results', + () async { + const doc = 'para one\n\n## Heading\n\n- a\n- b\n\ndone'; + final chunks = [ + for (var i = 0; i < doc.length; i += 4) + doc.substring(i, min(i + 4, doc.length)), + ]; + final results = + await Stream.fromIterable(chunks).toMarkdown().toList(); + expect(results, isNotEmpty); + expect(_sig(results.last), _sig(Markdown.fromString(doc))); + // Every intermediate emission matches the batch parse of its prefix. + final acc = StringBuffer(); + for (var i = 0; i < chunks.length; i++) { + acc.write(chunks[i]); + expect(_sig(results[i]), _sig(Markdown.fromString(acc.toString()))); + } + }); + + test('Stream.toMarkdown threads inlineMath through', () async { + const doc = r'$\alpha$ and $x^2$'; + final results = await Stream.value(doc) + .toMarkdown(decoder: const MarkdownDecoder(inlineMath: true)) + .toList(); + expect(_sig(results.last), + _sig(Markdown.fromString(doc, inlineMath: true))); + }); + }); + }); + +/// A structural signature of a [Markdown] value: source + a deep dump of every +/// block (type, level/marker/checked/alignment, inline spans with their exact +/// offsets and styles). Two [Markdown]s with equal signatures are equivalent +/// for every observable purpose, which node identity equality cannot express. +String _sig(Markdown md) { + final b = StringBuffer()..writeln('SRC<<${md.markdown}>>'); + for (final block in md.blocks) { + b.writeln(_blockSig(block)); + } + return b.toString(); +} + +String _blockSig(MD$Block block) => block.map( + paragraph: (p) => 'P|${_spans(p.spans)}', + heading: (h) => 'H${h.level}|${_spans(h.spans)}', + quote: (q) => 'Q${q.indent}|${_spans(q.spans)}', + alert: (a) => 'A[${a.alert.marker}]|${_spans(a.spans)}', + code: (c) => 'C[${c.language}]<<${c.text}>>', + list: (l) => 'L|${l.items.map(_itemSig).join(';')}', + divider: (_) => 'DIV', + table: (t) => 'T|${_rowSig(t.header)}|' + '${t.alignments.join(',')}|' + '${t.rows.map(_rowSig).join(';')}', + spacer: (s) => 'S${s.count}', + ); + +String _itemSig(MD$ListItem it) => + '{${it.indent}/${it.marker}/${it.checked}/${_spans(it.spans)}' + '[${it.children.map(_itemSig).join(',')}]}'; + +String _rowSig(MD$TableRow row) => row.cells.map(_spans).join('ยฆ'); + +String _spans(List spans) => spans + .map((s) => '${s.start}:${s.end}:${s.style.value}:${s.text}') + .join('ยง'); + +/// A varied document that hits most block types in one parse. +const String _mixed = ''' +# Streaming demo + +A short intro paragraph with **bold**, _italic_ and `code`. + +> [!TIP] +> Blocks freeze once a blank line proves they are complete. + +- one +- two + - nested +- [x] a task + +| Feature | Incremental | +| ------- | :---------: | +| Parse | yes | +| Paint | n/a | + +```dart +final md = StreamingMarkdownParser(); +md.add('# Hello'); +``` + +--- + +The end. +'''; diff --git a/test/selection/markup_formatter_test.dart b/test/selection/markup_formatter_test.dart new file mode 100644 index 0000000..4fa2ad9 --- /dev/null +++ b/test/selection/markup_formatter_test.dart @@ -0,0 +1,198 @@ +import 'package:flutter/painting.dart' show TextRange; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Full-block selection of a single-document source, formatted as Markdown. +String _markup(String source, + [MarkdownMarkupFormatter formatter = const MarkdownMarkupFormatter()]) { + final c = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'doc', model: Markdown.fromString(source)), + ]) + ..selectAll(); + return c.getText(formatter); +} + +void main() { + group('MarkdownMarkupFormatter โ€” whole-block reconstruction', () { + test('nested unordered list keeps indentation and markers', () { + expect( + _markup('- one\n- two\n - nested\n- three'), + '- one\n- two\n - nested\n- three', + ); + }); + + test('ordered list keeps numbers, nested indented one level', () { + expect( + _markup('1. first\n2. second\n 1. sub'), + '1. first\n2. second\n 1. sub', + ); + }); + + test('task list keeps checkboxes', () { + expect(_markup('- [x] done\n- [ ] todo'), '- [x] done\n- [ ] todo'); + }); + + test('headings keep their level', () { + expect(_markup('# Title'), '# Title'); + expect(_markup('### Deep'), '### Deep'); + }); + + test('blockquote is prefixed', () { + expect(_markup('> quoted line'), '> quoted line'); + }); + + test('alert emits its marker and prefixed body', () { + expect( + _markup('> [!NOTE]\n> Body one\n> Body two'), + '> [!NOTE]\n> Body one\n> Body two', + ); + }); + + test('code block is fenced with its language', () { + expect(_markup('```dart\nvar x = 1;\n```'), '```dart\nvar x = 1;\n```'); + }); + + test('code block without a language uses a bare fence', () { + expect(_markup('```\nplain\n```'), '```\nplain\n```'); + }); + + test('table becomes a pipe table with a delimiter row', () { + expect( + _markup('| a | b |\n|---|---|\n| 1 | 2 |'), + '| a | b |\n| --- | --- |\n| 1 | 2 |', + ); + }); + + test('custom listIndent controls nesting width', () { + expect( + _markup( + '- a\n - b', + const MarkdownMarkupFormatter(listIndent: ' '), + ), + '- a\n - b', + ); + }); + }); + + group('MarkdownMarkupFormatter โ€” separators', () { + // Blocks: 0 = heading, 1 = spacer, 2 = paragraph. + final doc = Markdown.fromString('# Heading\n\nA paragraph.'); + + MarkdownSelectionController one() => MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'doc', model: doc), + ]); + + test('blocks within a document join with a blank line by default', () { + final c = one()..selectAll(); + expect(c.getText(const MarkdownMarkupFormatter()), + '# Heading\n\nA paragraph.'); + }); + + test('documents join with documentSeparator', () { + final c = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'a', model: Markdown.fromString('# One')), + MarkdownDocumentRef(id: 'b', model: Markdown.fromString('- x\n- y')), + ]) + ..selectAll(); + expect(c.getText(const MarkdownMarkupFormatter()), '# One\n\n- x\n- y'); + }); + + test('separators are configurable', () { + final c = one()..selectAll(); + expect( + c.getText(const MarkdownMarkupFormatter(blockSeparator: '\n')), + '# Heading\nA paragraph.', + ); + }); + }); + + group('MarkdownMarkupFormatter โ€” partial boundary fallback', () { + test('a partially selected list falls back to plain sliced text', () { + final c = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'doc', + model: Markdown.fromString('- one\n- two\n - nested\n- three')), + ]); + // Rendered text is 'one\ntwo\nnested\nthree'; select only 'one\ntwo'. + c.selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'doc', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'doc', blockIndex: 0, offset: 7), + ); + // No markers reconstructed โ€” the plain slice is copied verbatim. + expect(c.getText(const MarkdownMarkupFormatter()), 'one\ntwo'); + }); + + test('fully selected boundary block still reconstructs', () { + // A selection that ends exactly at the block length is "whole". + final model = Markdown.fromString('- a\n- b'); + final c = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'doc', model: model), + ]); + final len = markdownBlockRenderedText(model.blocks.first).length; + c.selection = MarkdownSelection( + base: + const MarkdownPosition(documentId: 'doc', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'doc', blockIndex: 0, offset: len), + ); + expect(c.getText(const MarkdownMarkupFormatter()), '- a\n- b'); + }); + }); + + group('MarkdownMarkupFormatter โ€” wiring', () { + test('can be installed as the controller default formatter', () { + final c = MarkdownSelectionController() + ..formatter = const MarkdownMarkupFormatter() + ..setDocuments([ + MarkdownDocumentRef(id: 'doc', model: Markdown.fromString('# Hi')), + ]) + ..selectAll(); + // getText() with no argument now uses the markup formatter. + expect(c.getText(), '# Hi'); + }); + + test('differs from the default plain formatter', () { + final markup = _markup('# Heading\n\n- a\n- b'); + final plain = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'doc', model: Markdown.fromString('# Heading\n\n- a\n- b')), + ]); + plain.selectAll(); + expect(markup, isNot(equals(plain.getText()))); + expect(markup.contains('# Heading'), isTrue); + expect(markup.contains('- a'), isTrue); + }); + + test('implements the MarkdownSelectionFormatter interface', () { + const MarkdownSelectionFormatter formatter = MarkdownMarkupFormatter(); + const content = + MarkdownSelectedContent(documents: []); + expect(formatter.format(content), isEmpty); + }); + + test('an empty selection formats to an empty string', () { + final c = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'doc', model: Markdown.fromString('# Hi')), + ]); + expect(c.getText(const MarkdownMarkupFormatter()), isEmpty); + }); + }); + + group('MarkdownMarkupFormatter โ€” sanity of renderedRange assumptions', () { + test('TextRange is exposed on selected blocks', () { + final c = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'doc', model: Markdown.fromString('hello')), + ]) + ..selectAll(); + final seg = c.selectedContent().documents.single.blocks.single; + expect(seg.renderedRange, const TextRange(start: 0, end: 5)); + }); + }); +} diff --git a/test/selection/selection_handles_test.dart b/test/selection/selection_handles_test.dart new file mode 100644 index 0000000..2989a9d --- /dev/null +++ b/test/selection/selection_handles_test.dart @@ -0,0 +1,255 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future _mouseDrag(WidgetTester tester, Offset from, Offset to) async { + final g = await tester.startGesture(from, kind: PointerDeviceKind.mouse); + await tester.pump(const Duration(milliseconds: 200)); + await g.moveTo(to); + await tester.pump(const Duration(milliseconds: 200)); + await g.up(); + await tester.pumpAndSettle(); +} + +Widget _wrap(MarkdownSelectionController controller, Widget child) => + MaterialApp( + home: Scaffold( + body: MarkdownSelectionScope(controller: controller, child: child), + ), + ); + +class _Doc extends StatelessWidget { + const _Doc(this.id); + final String id; + + @override + Widget build(BuildContext context) { + final controller = MarkdownSelectionScope.of(context); + final model = controller.documents.firstWhere((d) => d.id == id).model; + return MarkdownWidget(markdown: model, documentId: id); + } +} + +void main() { + group('selection handles', () { + testWidgets('moveSelectionEdgeToGlobal adjusts the moving edge', + (tester) async { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + controller.selectAll(); + final fullLength = controller.getText().length; + expect(fullLength, 22); + + // Pull the end edge back to near the start of the line. + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + controller.moveSelectionEdgeToGlobal(tl + const Offset(40, 8), + isStart: false); + await tester.pump(); + + final text = controller.getText(); + expect(text.length, lessThan(fullLength)); + expect('Hello selectable world', startsWith(text)); + }); + + // Ordered (start, end) rendered-text offsets of a single-block selection. + (int, int) endpoints(MarkdownSelectionController c) { + final sel = c.selection!; + final a = sel.base.offset, b = sel.extent.offset; + return a <= b ? (a, b) : (b, a); + } + + testWidgets('moveSelectionEdgeToGlobal(isStart: true) moves only the start', + (tester) async { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + controller.selectAll(); // d#0@0 -> d#0@22 + await tester.pump(); + const full = 'Hello selectable world'; + expect(controller.getText(), full); + + // Drag the reading-order START edge rightwards into the line. + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + controller.moveSelectionEdgeToGlobal(tl + const Offset(120, 8), + isStart: true); + await tester.pump(); + + final (start, end) = endpoints(controller); + expect(end, 22, reason: 'the end edge stayed fixed at the line end'); + expect(start, greaterThan(0), reason: 'the start edge moved inward'); + expect(start, lessThan(22)); + final text = controller.getText(); + expect(text.length, lessThan(full.length)); + expect(full.endsWith(text), isTrue, + reason: 'moving the start keeps a suffix of the line'); + }); + + testWidgets('moveSelectionEdgeToGlobal(isStart: false) moves only the end', + (tester) async { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + controller.selectAll(); // d#0@0 -> d#0@22 + await tester.pump(); + const full = 'Hello selectable world'; + + // Drag the reading-order END edge leftwards into the middle of the line. + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + controller.moveSelectionEdgeToGlobal(tl + const Offset(120, 8), + isStart: false); + await tester.pump(); + + final (start, end) = endpoints(controller); + expect(start, 0, reason: 'the start edge stayed fixed at the line start'); + expect(end, greaterThan(0), reason: 'the end edge moved inward'); + expect(end, lessThan(22)); + final text = controller.getText(); + expect(text.length, lessThan(full.length)); + expect(full.startsWith(text), isTrue, + reason: 'moving the end keeps a prefix of the line'); + }); + + testWidgets('a handle drag past the other edge never collapses', + (tester) async { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + controller.selectAll(); + await tester.pump(); + const full = 'Hello selectable world'; + final before = controller.selection; + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + // Drag the START edge past the END (far right) โ€” would collapse, so the + // move is dropped and the selection is left untouched. + controller.moveSelectionEdgeToGlobal(tl + const Offset(399, 8), + isStart: true); + await tester.pump(); + expect(controller.selection, before, + reason: 'a collapsing move is a no-op'); + expect(controller.getText(), full); + expect(controller.selection!.isCollapsed, isFalse); + + // Symmetric: drag the END edge past the START (far left) โ€” also dropped. + controller.moveSelectionEdgeToGlobal(tl + const Offset(1, 8), + isStart: false); + await tester.pump(); + expect(controller.selection, before); + expect(controller.getText(), full); + expect(controller.selection!.isCollapsed, isFalse); + }); + + testWidgets('touch platform shows draggable handles for a selection', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + // NOTE: reset via try/finally rather than addTearDown โ€” in this Flutter + // version the framework's debugAssertAllFoundationVarsUnset check runs + // before user tearDowns, so a tearDown reset arrives too late and fails. + try { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(1, 3), br - const Offset(1, 3)); + + expect(controller.getText(), isNotEmpty); + // Exactly two handles (start + end) are composited to follow the + // content โ€” assert the pair specifically, not merely "some widgets". + expect(find.byType(CompositedTransformFollower), findsNWidgets(2)); + expect(tester.takeException(), isNull); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + + testWidgets('desktop platform shows no selection handles', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + try { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(1, 3), br - const Offset(1, 3)); + + expect(controller.getText(), isNotEmpty); + expect(find.byType(CompositedTransformFollower), findsNothing); + expect(tester.takeException(), isNull); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + }); +} diff --git a/test/selection/selection_keyboard_test.dart b/test/selection/selection_keyboard_test.dart new file mode 100644 index 0000000..a7e5ca1 --- /dev/null +++ b/test/selection/selection_keyboard_test.dart @@ -0,0 +1,423 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget _wrap( + MarkdownSelectionController controller, + Widget child, { + FocusNode? focusNode, +}) => + MaterialApp( + home: Scaffold( + body: MarkdownSelectionScope( + controller: controller, + focusNode: focusNode, + child: child, + ), + ), + ); + +class _Doc extends StatelessWidget { + const _Doc(this.id); + final String id; + + @override + Widget build(BuildContext context) { + final controller = MarkdownSelectionScope.of(context); + final model = controller.documents.firstWhere((d) => d.id == id).model; + return MarkdownWidget(markdown: model, documentId: id); + } +} + +void main() { + group('keyboard shortcuts', () { + testWidgets('Ctrl+A selects all', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + final md = Markdown.fromString('Hello keyboard world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + final focus = FocusNode(); + addTearDown(focus.dispose); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + focusNode: focus, + )); + await tester.pumpAndSettle(); + focus.requestFocus(); + await tester.pump(); + + await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.keyA); + await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); + await tester.pump(); + + debugDefaultTargetPlatformOverride = null; + expect(controller.getText(), 'Hello keyboard world'); + }); + + testWidgets('Esc clears the selection', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + final md = Markdown.fromString('Hello keyboard world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selectAll(); + final focus = FocusNode(); + addTearDown(focus.dispose); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + focusNode: focus, + )); + await tester.pumpAndSettle(); + focus.requestFocus(); + await tester.pump(); + expect(controller.getText(), isNotEmpty); + + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pump(); + + debugDefaultTargetPlatformOverride = null; + expect(controller.selection, isNull); + }); + + testWidgets('Shift+ArrowRight extends the selection by a character', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + final md = Markdown.fromString('Hello'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selection = const MarkdownSelection.collapsed( + MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0)); + final focus = FocusNode(); + addTearDown(focus.dispose); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + focusNode: focus, + )); + await tester.pumpAndSettle(); + focus.requestFocus(); + await tester.pump(); + + await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); + await tester.pump(); + + debugDefaultTargetPlatformOverride = null; + expect(controller.selection!.extent.offset, 1); + expect(controller.getText(), 'H'); + }); + + testWidgets('Shift+ArrowRight extends across a block boundary', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + // Reset via try/finally (not addTearDown): the framework's foundation-var + // invariant check runs before user tearDowns in this Flutter version. + try { + // '# Title\nBody text here' โ†’ block 0 heading "Title" (len 5), block 1 + // paragraph "Body text here" โ€” adjacent blocks with no spacer between. + final md = Markdown.fromString('# Title\nBody text here'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 5), + ); + final focus = FocusNode(); + addTearDown(focus.dispose); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + focusNode: focus, + )); + await tester.pumpAndSettle(); + focus.requestFocus(); + await tester.pump(); + + await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); + // pump (not pumpAndSettle) to avoid overlay/magnifier animation loops. + await tester.pump(); + + final extent = controller.selection!.extent; + expect(extent.documentId, 'd'); + expect(extent.blockIndex, 1, + reason: 'the extent crossed from block 0 into block 1'); + expect(extent.offset, 0, reason: 'offset resets to block start'); + expect(controller.getText(), 'Title'); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + + testWidgets('Ctrl+C copies the selection to the clipboard', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + final md = Markdown.fromString('Copy this text'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selectAll(); + final focus = FocusNode(); + addTearDown(focus.dispose); + + final data = []; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (call) async { + if (call.method == 'Clipboard.setData') data.add(call); + return null; + }, + ); + addTearDown(() => tester.binding.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null)); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + focusNode: focus, + )); + await tester.pumpAndSettle(); + focus.requestFocus(); + await tester.pump(); + + await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.keyC); + await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); + await tester.pumpAndSettle(); + + debugDefaultTargetPlatformOverride = null; + expect(data, isNotEmpty); + expect(data.first.arguments['text'], 'Copy this text'); + }); + }); + + group('context toolbar', () { + testWidgets('right-click over a selection shows a Copy button', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + final md = Markdown.fromString('Toolbar target text'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selectAll(); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + )); + await tester.pumpAndSettle(); + + final center = tester.getCenter(find.byType(MarkdownWidget)); + final g = await tester.startGesture(center, + kind: PointerDeviceKind.mouse, buttons: kSecondaryMouseButton); + await g.up(); + await tester.pumpAndSettle(); + + debugDefaultTargetPlatformOverride = null; + expect(find.text('Copy'), findsOneWidget); + expect(find.text('Select all'), findsOneWidget); + expect(tester.takeException(), isNull); + }); + + testWidgets('showToolbar/hideToolbar via the scope state', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + final md = Markdown.fromString('State driven toolbar'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selectAll(); + + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, child: _Doc('d')), + )); + await tester.pumpAndSettle(); + + final state = tester.state( + find.byType(MarkdownSelectionScope)); + state.showToolbar(); + await tester.pumpAndSettle(); + expect(find.text('Copy'), findsOneWidget); + expect(state.toolbarIsVisible, isTrue); + + state.hideToolbar(); + await tester.pumpAndSettle(); + + debugDefaultTargetPlatformOverride = null; + expect(find.text('Copy'), findsNothing); + expect(state.toolbarIsVisible, isFalse); + }); + }); + + // Deterministic, geometry-free coverage of the controller's keyboard-driven + // extension primitives (what the Shift+arrow Actions delegate to). + group('keyboard extension within a block', () { + MarkdownSelectionController single() => MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Hello selectable world')), + ]); // one paragraph block, rendered text length 22 + + test('extendSelectionByCharacter(forward: false) shrinks by one char', () { + final c = single() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 5), + ); + c.extendSelectionByCharacter(forward: false); + expect(c.selection!.extent.offset, 4); + expect(c.getText(), 'Hell'); + }); + + test('extendSelectionByWord(forward: true) grows one word at a time', () { + final c = single() + ..selection = const MarkdownSelection.collapsed( + MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0)); + c.extendSelectionByWord(forward: true); + expect(c.getText(), 'Hello'); + c.extendSelectionByWord(forward: true); + expect(c.getText(), 'Hello selectable'); + expect(c.selection!.extent.offset, 16); + }); + + test('extendSelectionByWord(forward: false) grabs the previous word', () { + final c = single() + ..selection = const MarkdownSelection.collapsed( + MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 22)); + c.extendSelectionByWord(forward: false); + expect(c.selection!.extent.offset, 17); + expect(c.getText(), 'world'); + }); + + test('extendSelectionToLineBreak reaches the block start/end', () { + final c = single() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 5), + ); + c.extendSelectionToLineBreak(forward: true); // End + expect(c.selection!.extent.offset, 22); + expect(c.getText(), 'Hello selectable world'); + + c.selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 10), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 22), + ); + c.extendSelectionToLineBreak(forward: false); // Home + expect(c.selection!.extent.offset, 0); + expect(c.getText(), 'Hello sele'); + }); + }); + + group('keyboard extension across blocks and documents', () { + // '# Title\nBody text here' โ†’ block 0 heading "Title" (len 5), block 1 + // paragraph "Body text here" (len 14): adjacent, no spacer between them. + MarkdownSelectionController headingDoc() => MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('# Title\nBody text here')), + ]); + + // Two paragraph blocks per doc (index 0 = paragraph, 1 = spacer, 2 = para). + MarkdownSelectionController twoDocs() => MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'a', + model: Markdown.fromString('Alpha one\n\nAlpha two'), + order: 0), + MarkdownDocumentRef( + id: 'b', + model: Markdown.fromString('Bravo one\n\nBravo two'), + order: 1), + ]); + + test('stepping forward off a block end moves into the next block', () { + final c = headingDoc() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 5), + ); + c.extendSelectionByCharacter(forward: true); + final e = c.selection!.extent; + expect(e.blockIndex, 1, reason: 'blockIndex incremented'); + expect(e.offset, 0, reason: 'offset reset to the block start'); + expect(e.documentId, 'd'); + expect(c.getText(), 'Title'); + }); + + test('stepping backward off a block start moves into the previous block', + () { + final c = headingDoc() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 1, offset: 14), + extent: MarkdownPosition(documentId: 'd', blockIndex: 1, offset: 0), + ); + c.extendSelectionByCharacter(forward: false); + final e = c.selection!.extent; + expect(e.blockIndex, 0, reason: 'blockIndex decremented into block 0'); + expect(e.offset, 5, reason: 'landed at the end of the previous block'); + expect(c.getText(), 'Body text here'); + }); + + test('stepping forward off the last block of doc A lands in doc B', () { + final c = twoDocs() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'a', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'a', blockIndex: 2, offset: 9), + ); + c.extendSelectionByCharacter(forward: true); + final e = c.selection!.extent; + expect(e.documentId, 'b', reason: 'crossed into the next document'); + expect(e.blockIndex, 0); + expect(e.offset, 0); + }); + + test('stepping backward off the first block of doc B lands in doc A', () { + final c = twoDocs() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'b', blockIndex: 0, offset: 5), + extent: MarkdownPosition(documentId: 'b', blockIndex: 0, offset: 0), + ); + c.extendSelectionByCharacter(forward: false); + final e = c.selection!.extent; + expect(e.documentId, 'a'); + expect(e.blockIndex, 2, reason: 'end of doc A (last non-empty block)'); + expect(e.offset, 9, reason: 'end of "Alpha two"'); + }); + + test('extendSelectionToDocumentBoundary reaches the first/last position', + () { + final c = twoDocs() + ..selection = const MarkdownSelection.collapsed( + MarkdownPosition(documentId: 'a', blockIndex: 0, offset: 0)); + c.extendSelectionToDocumentBoundary(forward: true); + final end = c.selection!.extent; + expect(end.documentId, 'b'); + expect(end.blockIndex, 2); + expect(end.offset, 9, reason: 'the very last position across all docs'); + expect(c.getText(), 'Alpha one\nAlpha two\n\nBravo one\nBravo two'); + + c.selection = const MarkdownSelection.collapsed( + MarkdownPosition(documentId: 'b', blockIndex: 2, offset: 9)); + c.extendSelectionToDocumentBoundary(forward: false); + final start = c.selection!.extent; + expect(start.documentId, 'a'); + expect(start.blockIndex, 0); + expect(start.offset, 0, reason: 'the first position across all docs'); + }); + }); +} diff --git a/test/selection/selection_test.dart b/test/selection/selection_test.dart new file mode 100644 index 0000000..b592025 --- /dev/null +++ b/test/selection/selection_test.dart @@ -0,0 +1,200 @@ +import 'package:flutter/painting.dart' show TextRange; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _UpperFormatter implements MarkdownSelectionFormatter { + const _UpperFormatter(); + @override + String format(MarkdownSelectedContent content) => content.documents + .expand((d) => d.blocks) + .map((b) => b.text.toUpperCase()) + .join(' | '); +} + +void main() { + group('selection core', () { + // Block indices: 0 = paragraph, 1 = spacer, 2 = paragraph. + final docA = Markdown.fromString('Alpha one\n\nAlpha two'); + final docB = Markdown.fromString('Bravo one\n\nBravo two'); + + MarkdownSelectionController two() => MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'a', model: docA), + MarkdownDocumentRef(id: 'b', model: docB), + ]); + + const full = MarkdownSelection( + base: MarkdownPosition(documentId: 'a', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'b', blockIndex: 2, offset: 9), + ); + + test('block linearization', () { + expect(markdownBlockRenderedText(docA.blocks[0]), 'Alpha one'); + expect(markdownBlockRenderedText(docA.blocks[1]), ''); // spacer + expect(markdownBlockRenderedText(docA.blocks[2]), 'Alpha two'); + final table = Markdown.fromString('| a | b |\n|---|---|\n| 1 | 2 |') + .blocks + .firstWhere((b) => b.type == 'table'); + expect(markdownBlockRenderedText(table), 'a\tb\n1\t2'); + + final list = Markdown.fromString('- one\n- two\n - nested\n- three') + .blocks + .firstWhere((b) => b.type == 'list'); + expect(markdownBlockRenderedText(list), 'one\ntwo\nnested\nthree'); + + final tasks = Markdown.fromString('- [x] done\n- [ ] todo') + .blocks + .firstWhere((b) => b.type == 'list'); + expect(markdownBlockRenderedText(tasks), 'done\ntodo'); + + // An empty leading item still occupies its own line, so the model offset + // space matches the painter's one-fragment-per-item layout. + final emptyLead = Markdown.fromString('- [ ]\n- [x] Done') + .blocks + .firstWhere((b) => b.type == 'list'); + expect(markdownBlockRenderedText(emptyLead), '\nDone'); + }); + + test('cross-document extraction with default formatter', () { + final c = two()..selection = full; + expect(c.getText(), 'Alpha one\nAlpha two\n\nBravo one\nBravo two'); + final content = c.selectedContent(); + expect(content.documents.length, 2); + expect(content.documents.first.blocks.map((b) => b.text).toList(), + ['Alpha one', 'Alpha two']); // spacer skipped + expect(content.documents.first.blocks.first.type, 'paragraph'); + }); + + test('partial slice', () { + final c = two() + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'a', blockIndex: 2, offset: 6), + extent: MarkdownPosition(documentId: 'b', blockIndex: 0, offset: 5), + ); + expect(c.getText(), 'two\n\nBravo'); + }); + + test('custom formatter is used', () { + final c = two()..selection = full; + expect(c.getText(const _UpperFormatter()), + 'ALPHA ONE | ALPHA TWO | BRAVO ONE | BRAVO TWO'); + }); + + test('reconcile: append keeps the anchor verbatim', () { + final c = two()..selection = full; + final before = c.getText(); + c.putDocument( + 'b', Markdown.fromString('Bravo one\n\nBravo two three\n\nEnd')); + expect(c.selection!.extent.blockIndex, 2); + expect(c.selection!.extent.offset, 9); + expect(c.getText(), before); + }); + + test('reconcile: content-anchored survives a front-insert', () { + final c = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'b', model: docB)]) + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'b', blockIndex: 2, offset: 0), + extent: MarkdownPosition(documentId: 'b', blockIndex: 2, offset: 9), + ); + expect(c.getText(), 'Bravo two'); + c.putDocument( + 'b', Markdown.fromString('HEADER\n\nBravo one\n\nBravo two')); + expect(c.getText(), 'Bravo two'); // relocated by content, not index + }); + + test('reconcile: appendFastPath drifts on a front-insert', () { + final c = MarkdownSelectionController( + reconciliation: const MarkdownReconciliationPolicy.appendFastPath(), + ) + ..setDocuments( + [MarkdownDocumentRef(id: 'b', model: docB)]) + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'b', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'b', blockIndex: 0, offset: 9), + ); + expect(c.getText(), 'Bravo one'); + c.putDocument( + 'b', Markdown.fromString('HEADER LINE\n\nBravo one\n\nBravo two')); + expect(c.getText(), isNot('Bravo one')); // clamped to new block 0 + }); + + test('reconcile: clearOnChange drops the selection', () { + final c = MarkdownSelectionController( + reconciliation: const MarkdownReconciliationPolicy.clearOnChange(), + ) + ..setDocuments( + [MarkdownDocumentRef(id: 'b', model: docB)]) + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'b', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'b', blockIndex: 2, offset: 9), + ); + c.putDocument('b', Markdown.fromString('Bravo one\n\nBravo two four')); + expect(c.selection, isNull); + }); + + test('selectAll and clear', () { + final c = two()..selectAll(); + expect(c.getText(), 'Alpha one\nAlpha two\n\nBravo one\nBravo two'); + c.clear(); + expect(c.selection, isNull); + expect(c.getText(), ''); + }); + + test('rangeFor', () { + final c = two()..selection = full; + expect(c.rangeFor('a', 0), const TextRange(start: 0, end: 9)); + expect(c.rangeFor('b', 0), const TextRange(start: 0, end: 9)); + expect(c.rangeFor('missing', 0), isNull); + }); + + test('removeDocument drops a touching selection', () { + final c = two()..selection = full; + c.removeDocument('a'); + expect(c.selection, isNull); + }); + + test('notifies listeners on selection change', () { + final c = two(); + var n = 0; + c.addListener(() => n++); + c.selection = full; + c.clear(); + expect(n, 2); + }); + }); + + group('word boundaries (double-click / long-press)', () { + (int, int) word(String text, int offset) => + MarkdownSelectionController.wordRangeIn(text, offset); + + test('empty text yields an empty range', () { + expect(word('', 0), (0, 0)); + }); + + test('caret inside a word grabs the whole word', () { + expect(word('Hello world', 2), (0, 5)); + expect(word('Hello world', 8), (6, 11)); + }); + + test('caret at a word edge prefers the adjacent word', () { + expect(word('Hello world', 5), (0, 5)); // end of "Hello" + expect(word('Hello world', 6), (6, 11)); // start of "world" + expect(word('Hello world', 11), (6, 11)); // very end + }); + + test('underscore and digits are part of a word', () { + expect(word('foo_bar2 baz', 1), (0, 8)); + }); + + test('non-ASCII letters stay in the word', () { + expect(word('cafรฉ crรจme', 1), (0, 4)); + }); + + test('punctuation forms its own run', () { + // "a===b": clicking on the punctuation run selects just the "===". + expect(word('a===b', 2), (1, 4)); + }); + }); +} diff --git a/test/selection/selection_widget_test.dart b/test/selection/selection_widget_test.dart new file mode 100644 index 0000000..f23d061 --- /dev/null +++ b/test/selection/selection_widget_test.dart @@ -0,0 +1,743 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future _mouseDrag(WidgetTester tester, Offset from, Offset to) async { + final g = await tester.startGesture(from, kind: PointerDeviceKind.mouse); + await tester.pump(const Duration(milliseconds: 200)); + await g.moveTo(to); + await tester.pump(const Duration(milliseconds: 200)); + await g.up(); + await tester.pumpAndSettle(); +} + +/// Taps [times] times in quick succession at [pos] (a mouse click, then a +/// double- or triple-click when times > 1). +Future _clicks(WidgetTester tester, Offset pos, int times) async { + for (var i = 0; i < times; i++) { + final g = await tester.startGesture(pos, kind: PointerDeviceKind.mouse); + await g.up(); + if (i < times - 1) await tester.pump(const Duration(milliseconds: 40)); + } + await tester.pumpAndSettle(); +} + +Widget _wrap(MarkdownSelectionController controller, Widget child) => + MaterialApp( + home: Scaffold( + body: MarkdownSelectionScope(controller: controller, child: child), + ), + ); + +void main() { + group('selection widget integration', () { + testWidgets('drag selects a single paragraph', (tester) async { + final md = Markdown.fromString('Hello selectable world'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(1, 3), br - const Offset(1, 3)); + + expect(controller.getText(), 'Hello selectable world'); + expect(tester.takeException(), isNull); + }); + + testWidgets('drag spans two MarkdownWidgets with a document separator', + (tester) async { + final a = Markdown.fromString('First message body'); + final b = Markdown.fromString('Second message body'); + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef(id: 'a', model: a), + MarkdownDocumentRef(id: 'b', model: b), + ]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_Doc('a'), _Doc('b')], + ), + ), + ), + )); + await tester.pumpAndSettle(); + + final first = find.byType(MarkdownWidget).first; + final last = find.byType(MarkdownWidget).last; + await _mouseDrag( + tester, + tester.getTopLeft(first) + const Offset(1, 3), + tester.getBottomRight(last) - const Offset(1, 3), + ); + + expect(controller.getText(), 'First message body\n\nSecond message body'); + expect(tester.takeException(), isNull); + }); + + testWidgets('cross-widget selection survives ListView disposal', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + for (var i = 0; i < 10; i++) + MarkdownDocumentRef( + id: 'm$i', + model: Markdown.fromString('Message number $i'), + order: i, + ), + ]); + final scroll = ScrollController(); + + await tester.pumpWidget(_wrap( + controller, + SizedBox( + height: 200, + child: ListView.builder( + controller: scroll, + itemCount: 10, + itemBuilder: (_, i) => SizedBox( + height: 80, + child: _Doc('m$i'), + ), + ), + ), + )); + await tester.pumpAndSettle(); + + final p0 = tester.getTopLeft(find.byType(MarkdownWidget).first) + + const Offset(1, 3); + final p1 = tester.getCenter(find.byType(MarkdownWidget).at(1)); + await _mouseDrag(tester, p0, p1); + final before = controller.getText(); + expect(before, contains('Message number 0')); + expect(before, contains('Message number 1')); + + scroll.jumpTo(80.0 * 7); // dispose the first messages + await tester.pumpAndSettle(); + expect( + find.byWidgetPredicate( + (w) => w is MarkdownWidget && w.documentId == 'm0'), + findsNothing); + + // Text is derived from the model registry โ†’ intact after disposal. + expect(controller.getText(), before); + expect(controller.getText(), contains('Message number 0')); + expect(tester.takeException(), isNull); + }); + + testWidgets('selecting in one controller clears the other (group)', + (tester) async { + final group = MarkdownSelectionGroup(); + final a = Markdown.fromString('Alpha body text'); + final b = Markdown.fromString('Bravo body text'); + final ca = MarkdownSelectionController(group: group) + ..setDocuments( + [MarkdownDocumentRef(id: 'a', model: a)]); + final cb = MarkdownSelectionController(group: group) + ..setDocuments( + [MarkdownDocumentRef(id: 'b', model: b)]); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MarkdownSelectionScope( + controller: ca, + child: const SizedBox(width: 400, child: _Doc('a')), + ), + MarkdownSelectionScope( + controller: cb, + child: const SizedBox(width: 400, child: _Doc('b')), + ), + ], + ), + ), + )); + await tester.pumpAndSettle(); + + // Select in controller B first. + final bw = find + .byWidgetPredicate((w) => w is MarkdownWidget && w.documentId == 'b'); + await _mouseDrag( + tester, + tester.getTopLeft(bw) + const Offset(1, 3), + tester.getBottomRight(bw) - const Offset(1, 3), + ); + expect(cb.getText(), isNotEmpty); + + // Now select in controller A โ€” B must be cleared. + final aw = find + .byWidgetPredicate((w) => w is MarkdownWidget && w.documentId == 'a'); + await _mouseDrag( + tester, + tester.getTopLeft(aw) + const Offset(1, 3), + tester.getBottomRight(aw) - const Offset(1, 3), + ); + expect(ca.getText(), isNotEmpty); + expect(cb.selection, isNull, + reason: 'group cleared the other controller'); + expect(tester.takeException(), isNull); + }); + + testWidgets('MarkdownWidget without documentId stays inert', + (tester) async { + final md = Markdown.fromString('Not selectable here'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'x', model: md)]); + await tester.pumpWidget(_wrap( + controller, + SizedBox(width: 400, child: MarkdownWidget(markdown: md)), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(1, 3), br - const Offset(1, 3)); + + expect(controller.getText(), '', reason: 'no documentId => inert'); + expect(tester.takeException(), isNull); + }); + + testWidgets('drag with only an empty (zero-size) document does not crash', + (tester) async { + // Regression: positionForGlobal's nearest-surface fallback used to invert + // num.clamp for a zero-area surface, throwing ArgumentError mid-drag. + final controller = MarkdownSelectionController() + ..setDocuments(const [ + MarkdownDocumentRef(id: 'e', model: Markdown.empty()), + ]); + await tester.pumpWidget(_wrap( + controller, + const SizedBox(width: 400, height: 200, child: _Doc('e')), + )); + await tester.pumpAndSettle(); + + await _mouseDrag(tester, const Offset(20, 20), const Offset(220, 160)); + expect(tester.takeException(), isNull); + expect(controller.getText(), ''); + }); + + testWidgets('drag past an empty document still selects a real one', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + const MarkdownDocumentRef( + id: 'empty', model: Markdown.empty(), order: 0), + MarkdownDocumentRef( + id: 'real', + model: Markdown.fromString('Real content here'), + order: 1), + ]); + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_Doc('empty'), _Doc('real')], + ), + ), + ), + )); + await tester.pumpAndSettle(); + + final realWidget = find.byWidgetPredicate( + (w) => w is MarkdownWidget && w.documentId == 'real'); + await _mouseDrag( + tester, + tester.getTopLeft(realWidget) + const Offset(1, 3), + tester.getBottomRight(realWidget) - const Offset(1, 3), + ); + expect(tester.takeException(), isNull); + expect(controller.getText(), 'Real content here'); + }); + + testWidgets('drag selects the cells of a table', (tester) async { + final md = + Markdown.fromString('| A | B |\n|---|---|\n| 1 | 2 |\n| 3 | 4 |'); + final table = md.blocks.firstWhere((b) => b.type == 'table'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(2, 2), br - const Offset(2, 2)); + + expect(controller.getText(), markdownBlockRenderedText(table)); + expect(controller.getText(), 'A\tB\n1\t2\n3\t4'); + expect(tester.takeException(), isNull); + }); + + testWidgets('drag selects the items of a list', (tester) async { + final md = Markdown.fromString('- alpha\n- beta\n- gamma'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(2, 2), br - const Offset(2, 2)); + + expect(controller.getText(), 'alpha\nbeta\ngamma'); + expect(tester.takeException(), isNull); + }); + + testWidgets('selection spanning a table includes its cells', + (tester) async { + final md = Markdown.fromString( + 'Intro line\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\nOutro line'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final tl = tester.getTopLeft(find.byType(MarkdownWidget)); + final br = tester.getBottomRight(find.byType(MarkdownWidget)); + await _mouseDrag( + tester, tl + const Offset(2, 2), br - const Offset(2, 2)); + + expect(controller.getText(), 'Intro line\nA\tB\n1\t2\nOutro line'); + expect(tester.takeException(), isNull); + }); + }); + + group('selection gestures', () { + Future pumpParagraph( + WidgetTester tester, + MarkdownSelectionController controller, + ) async { + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + return tester.getTopLeft(find.byType(MarkdownWidget)); + } + + testWidgets('double-click selects the word under the pointer', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Hello selectable world')), + ]); + final tl = await pumpParagraph(tester, controller); + + await _clicks(tester, tl + const Offset(8, 8), 2); + expect(controller.getText(), 'Hello'); + expect(tester.takeException(), isNull); + }); + + testWidgets('double-click keeps intra-word punctuation (apostrophe)', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString("can't stop here")), + ]); + final tl = await pumpParagraph(tester, controller); + + await _clicks(tester, tl + const Offset(8, 8), 2); + // The platform word segmentation keeps the apostrophe inside the word, + // unlike the plain punctuation-splitting heuristic. + expect(controller.getText(), "can't"); + expect(tester.takeException(), isNull); + }); + + testWidgets('touch double-tap selects a word and shows the toolbar', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Hello selectable world')), + ]); + final tl = await pumpParagraph(tester, controller); + + final pos = tl + const Offset(8, 8); + await tester.tapAt(pos); // default gesture kind is touch + await tester.pump(const Duration(milliseconds: 40)); + await tester.tapAt(pos); + await tester.pumpAndSettle(); + + expect(controller.getText(), 'Hello'); + final state = tester.state( + find.byType(MarkdownSelectionScope)); + expect(state.toolbarIsVisible, isTrue, + reason: 'a mobile double-tap pops the selection toolbar'); + expect(tester.takeException(), isNull); + }); + + testWidgets('triple-click selects the whole block', (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Hello selectable world')), + ]); + final tl = await pumpParagraph(tester, controller); + + await _clicks(tester, tl + const Offset(8, 8), 3); + expect(controller.getText(), 'Hello selectable world'); + expect(tester.takeException(), isNull); + }); + + testWidgets('single click clears an existing selection', (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Hello selectable world')), + ]); + final tl = await pumpParagraph(tester, controller); + + await _clicks(tester, tl + const Offset(8, 8), 2); + expect(controller.getText(), isNotEmpty); + + await _clicks(tester, tl + const Offset(8, 8), 1); + expect(controller.getText(), isEmpty, + reason: 'a single click collapses the selection'); + expect(tester.takeException(), isNull); + }); + + testWidgets('shift-click grows the selection toward the clicked point', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Hello selectable world')), + ]); + final tl = await pumpParagraph(tester, controller); + + // Caret at the very start of the line. + await _clicks(tester, tl + const Offset(1, 8), 1); + expect(controller.getText(), isEmpty, reason: 'a single click collapses'); + + // Shift-click into the middle grows a ranged selection from the caret. + await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); + await _clicks(tester, tl + const Offset(120, 8), 1); + await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); + final mid = controller.getText(); + expect(mid, isNotEmpty); + expect('Hello selectable world'.startsWith(mid), isTrue, + reason: 'the selection is a prefix anchored at the start caret'); + expect(mid.length, lessThan('Hello selectable world'.length)); + + // Shift-click past the line end grows it to the whole line. + await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); + await _clicks(tester, tl + const Offset(399, 8), 1); + await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); + final grown = controller.getText(); + expect(grown, 'Hello selectable world'); + expect(grown.length, greaterThan(mid.length), + reason: 'clicking further right extends the selection'); + expect(grown.startsWith(mid), isTrue); + expect(tester.takeException(), isNull); + }); + }); + + group('selection toolbar buttons', () { + testWidgets('tapping Copy in the toolbar copies the selection', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + // Reset via try/finally (not addTearDown): the framework's foundation-var + // invariant check runs before user tearDowns in this Flutter version. + try { + final md = Markdown.fromString('One two\n\nThree four'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + ..selectAll(); + + final data = []; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (call) async { + if (call.method == 'Clipboard.setData') data.add(call); + return null; + }, + ); + addTearDown(() => tester.binding.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null)); + + await tester.pumpWidget( + _wrap(controller, const SizedBox(width: 400, child: _Doc('d')))); + await tester.pumpAndSettle(); + + final state = tester.state( + find.byType(MarkdownSelectionScope)); + state.showToolbar(); + await tester.pumpAndSettle(); + expect(find.text('Copy'), findsOneWidget); + + // Tap the real toolbar button end-to-end. + await tester.tap(find.text('Copy')); + await tester.pumpAndSettle(); + + expect(data, isNotEmpty); + expect(data.first.arguments['text'], 'One two\nThree four'); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + + testWidgets('tapping Select all in the toolbar selects every document', + (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + try { + final md = Markdown.fromString('One two\n\nThree four'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]) + // Start with only the first paragraph selected. + ..selection = const MarkdownSelection( + base: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 7), + ); + + await tester.pumpWidget( + _wrap(controller, const SizedBox(width: 400, child: _Doc('d')))); + await tester.pumpAndSettle(); + expect(controller.getText(), 'One two'); + + final state = tester.state( + find.byType(MarkdownSelectionScope)); + state.showToolbar(); + await tester.pumpAndSettle(); + expect(find.text('Select all'), findsOneWidget); + + await tester.tap(find.text('Select all')); + await tester.pumpAndSettle(); + + expect(controller.getText(), 'One two\nThree four', + reason: 'select all now spans both paragraphs'); + final sel = controller.selection!; + expect(sel.base.offset, 0); + expect(sel.extent.blockIndex, 2); + expect(sel.extent.offset, 10); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + }); + + group('selection highlight', () { + testWidgets('paints over an opaque code-block background', (tester) async { + // Regression: the highlight used to draw BENEATH the block picture, so a + // code fence's opaque background hid it. It now draws on top. + final md = Markdown.fromString('```\ncode\n```'); + final controller = MarkdownSelectionController() + ..setDocuments( + [MarkdownDocumentRef(id: 'd', model: md)]); + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: MarkdownSelectionScope( + controller: controller, + selectionColor: const Color(0x80FF0000), // translucent red + child: const Align( + alignment: Alignment.topLeft, + child: RepaintBoundary( + key: Key('capture'), + child: SizedBox(width: 400, child: _Doc('d')), + ), + ), + ), + ), + )); + await tester.pumpAndSettle(); + + // Select the whole code block. + final len = markdownBlockRenderedText(md.blocks.first).length; + controller.selection = MarkdownSelection( + base: const MarkdownPosition(documentId: 'd', blockIndex: 0, offset: 0), + extent: MarkdownPosition(documentId: 'd', blockIndex: 0, offset: len), + ); + await tester.pumpAndSettle(); + + // Sample a pixel over the first code glyph (block padding is 8px). + // `toByteData` drives the engine, so it must run under `runAsync`. + late final int r, g, b; + await tester.runAsync(() async { + final boundary = tester.renderObject( + find.byKey(const Key('capture'))); + final image = boundary.toImageSync(); + final width = image.width; + final data = await image.toByteData(); + image.dispose(); + const x = 12, y = 12; + final i = (y * width + x) * 4; + r = data!.getUint8(i); + g = data.getUint8(i + 1); + b = data.getUint8(i + 2); + }); + + expect(r, greaterThan(g + 20), + reason: 'the red highlight must tint the code background'); + expect(r, greaterThan(b + 20)); + expect(tester.takeException(), isNull); + }); + }); + + group('selection cursor', () { + testWidgets('selectable content shows the text (I-beam) cursor', + (tester) async { + final controller = MarkdownSelectionController() + ..setDocuments([ + MarkdownDocumentRef( + id: 'd', model: Markdown.fromString('Selectable text here')), + ]); + await tester.pumpWidget(_wrap( + controller, + const Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: _Doc('d')), + ), + )); + await tester.pumpAndSettle(); + + final gesture = + await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byType(MarkdownWidget))); + await tester.pumpAndSettle(); + + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.text, + ); + }); + + testWidgets('an actionable link shows the click (hand) cursor', + (tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: MarkdownTheme( + data: MarkdownThemeData( + textStyle: const TextStyle(fontSize: 14), + onLinkTap: (_, __) {}, + ), + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + child: MarkdownWidget( + markdown: + Markdown.fromString('[click me](https://example.com)')), + ), + ), + ), + ), + )); + await tester.pumpAndSettle(); + + final gesture = + await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + // Hover over the link glyphs (near the start of the line). + await gesture.moveTo( + tester.getTopLeft(find.byType(MarkdownWidget)) + const Offset(8, 8)); + await tester.pumpAndSettle(); + + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.click, + ); + }); + + testWidgets('inert content keeps the default cursor', (tester) async { + final md = Markdown.fromString('Not selectable'); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Align( + alignment: Alignment.topLeft, + child: SizedBox(width: 400, child: MarkdownWidget(markdown: md)), + ), + ), + )); + await tester.pumpAndSettle(); + + final gesture = + await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byType(MarkdownWidget))); + await tester.pumpAndSettle(); + + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + }); + }); +} + +/// A MarkdownWidget that resolves its controller from the ambient scope. +class _Doc extends StatelessWidget { + const _Doc(this.id); + final String id; + + @override + Widget build(BuildContext context) { + final controller = MarkdownSelectionScope.of(context); + final model = controller.documents.firstWhere((d) => d.id == id).model; + return MarkdownWidget(markdown: model, documentId: id); + } +} diff --git a/test/unit_test.dart b/test/unit_test.dart index 7552576..f6dce08 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -9,6 +9,13 @@ import 'parser/inline_test.dart' as inline_test; import 'parser/math_test.dart' as math_test; import 'parser/parser_test.dart' as parser_test; import 'parser/regression_test.dart' as regression_test; +import 'parser/streaming_test.dart' as streaming_test; +import 'selection/markup_formatter_test.dart' as markup_formatter_test; +import 'selection/selection_handles_test.dart' as selection_handles_test; +import 'selection/selection_keyboard_test.dart' as selection_keyboard_test; +import 'selection/selection_test.dart' as selection_test; +import 'selection/selection_widget_test.dart' as selection_widget_test; +import 'highlight/highlight_test.dart' as highlight_test; import 'theme/theme_test.dart' as theme_test; import 'widget/render_test.dart' as render_test; import 'widget/widget_test.dart' as widget_test; @@ -21,9 +28,16 @@ void main() => group('Unit', () { edge_cases_test.main(); math_test.main(); regression_test.main(); + streaming_test.main(); golden_test.main(); nodes_test.main(); + highlight_test.main(); theme_test.main(); + selection_test.main(); + markup_formatter_test.main(); + selection_widget_test.main(); + selection_keyboard_test.main(); + selection_handles_test.main(); render_test.main(); widget_test.main(); }); diff --git a/tool/highlight_codegen/README.md b/tool/highlight_codegen/README.md new file mode 100644 index 0000000..d1d940a --- /dev/null +++ b/tool/highlight_codegen/README.md @@ -0,0 +1,61 @@ +# Syntax-highlight grammar codegen + +Generates the tree-shakeable Dart grammars under `lib/highlight/` (consumed by +`package:flutter_md/highlight.dart`) from upstream +[Prism](https://prismjs.com/) grammar definitions. Dev-only tooling โ€” not part +of the published package. + +## Regenerate + +```sh +cd tool/highlight_codegen +npm install # pins prismjs (see package.json) +node dump.cjs # snapshot grammars -> grammars.json +node codegen.cjs # emit lib/highlight/.dart (+ all.dart) +cd ../.. && dart format lib/highlight +``` + +The set of languages lives in `languages.json`. `dump.cjs` loads them via +Prism's `loadLanguages`, which pulls in every transitive dependency (e.g. `tsx` +drags in `jsx`, `typescript`, `javascript`, `markup`), and snapshots the **full +closure** โ€” Prism resolves `extend` / `insertBefore` at load time, so the +snapshot is the fully built grammar. Each distinct grammar object becomes one +file named by its canonical name; aliases (jsโ†’javascript, shโ†’bash, โ€ฆ) are +recorded and surface only in `all.dart`. + +To add languages, append to `languages.json` and re-run. + +## Outputs + +- `lib/highlight/.dart` โ€” one `Highlight.grammar` per language. +- `lib/highlight/all.dart` โ€” a convenience `allHighlightLanguages` map (canonical + names + aliases) that references **every** grammar. Referencing it prevents + unused languages from tree-shaking; it's for demos/tooling (the example's + Highlight tab). + +## How it stays tree-shakeable + +- Each language becomes its own library (`lib/highlight/.dart`) exposing a + single `Highlight.grammar`. Importing one never references the others. +- Grammars are hoisted into per-object lazy `final`s and referenced through + thunks (`inside: () => _gN`), which handles self/cross-references and cyclic + sub-grammars (e.g. bash) without a central registry. +- There is deliberately **no** `Map`/`enum`/`switch` that enumerates all + languages: the app assembles the `{ 'lang': grammar }` map at its own call + site, so only the grammars it names survive compilation. + +## Notes / known limitations + +- Regex sources are emitted as escaped Dart string literals. Dart's `RegExp` + (Irregexp) is JS-compatible, so most patterns port verbatim; only the `i` + flag appears in the current language set. +- The tokenizer relies on grammar ordering rather than a cross-segment `greedy` + re-scan (the `greedy` flag is retained but advisory). This can mis-highlight + rare pathological cases; it is correct for typical code. +- Grammar keys with `undefined` values (e.g. Dart's inherited `string` hole) + are skipped. + +## Attribution + +Grammar definitions are adapted from [PrismJS](https://github.com/PrismJS/prism), +which is distributed under the MIT License. diff --git a/tool/highlight_codegen/codegen.cjs b/tool/highlight_codegen/codegen.cjs new file mode 100644 index 0000000..5516526 --- /dev/null +++ b/tool/highlight_codegen/codegen.cjs @@ -0,0 +1,150 @@ +// Emit tree-shakeable Dart grammar files from grammars.json. +const fs = require('fs'); +const path = require('path'); + +const { languages, aliases } = require('./grammars.json'); +const OUT_DIR = path.resolve(__dirname, '../../lib/highlight'); +fs.mkdirSync(OUT_DIR, { recursive: true }); + +const pascal = (name) => + name.split(/[-_]/).map((s) => (s ? s[0].toUpperCase() + s.slice(1) : '')).join(''); +const cls = (name) => `Highlight${pascal(name)}`; +// Dart file names must be lower_case_with_underscores. +const fileName = (name) => name.replace(/-/g, '_'); + +// Dart double-quoted string literal for an arbitrary regex source. +function dartStr(s) { + let r = ''; + for (const ch of s) { + if (ch === '\\') r += '\\\\'; + else if (ch === '"') r += '\\"'; + else if (ch === '$') r += '\\$'; + else if (ch === '\n') r += '\\n'; + else if (ch === '\r') r += '\\r'; + else if (ch === '\t') r += '\\t'; + else r += ch; + } + return `"${r}"`; +} + +function regexExpr(node) { + const args = [dartStr(node.s)]; + const f = node.f || ''; + if (f.includes('i')) args.push('caseSensitive: false'); + if (f.includes('m')) args.push('multiLine: true'); + if (f.includes('s')) args.push('dotAll: true'); + if (f.includes('u')) args.push('unicode: true'); + return `compileHighlightPattern(${args.join(', ')})`; +} + +function refExpr(ref, rootName, externals) { + if (!ref) return null; + if (ref.ref !== undefined) return `() => _g${ref.ref}`; + if (ref.langref !== undefined) { + if (ref.langref === rootName) return '() => _g0'; + externals.add(ref.langref); + return `() => ${cls(ref.langref)}.grammar`; + } + return null; +} + +function tokensForEntry(name, value, rootName, externals) { + const items = value.t === 'arr' ? value.items : [value]; + const out = []; + for (const it of items) { + if (it.t === 're') { + out.push(`GrammarToken(${dartStr(name)}, ${regexExpr(it)}),`); + continue; + } + if (it.t !== 'tok') continue; + const parts = [dartStr(name), regexExpr(it)]; + if (it.lb) parts.push('lookbehind: true'); + if (it.g) parts.push('greedy: true'); + if (it.alias != null) { + const a = Array.isArray(it.alias) ? it.alias[0] : it.alias; + if (typeof a === 'string') parts.push(`alias: ${dartStr(a)}`); + } + const inside = refExpr(it.inside, rootName, externals); + if (inside) parts.push(`inside: ${inside}`); + out.push(`GrammarToken(${parts.join(', ')}),`); + } + return out; +} + +function generate(lang) { + const { rootName, defs } = languages[lang]; + const externals = new Set(); + const decls = defs.map((def, id) => { + const lines = []; + for (const e of def.entries) { + for (const l of tokensForEntry(e.name, e.value, rootName, externals)) { + lines.push(` ${l}`); + } + } + const rest = refExpr(def.rest, rootName, externals); + const restArg = rest ? `, rest: ${rest}` : ''; + return `final Grammar _g${id} = Grammar([\n${lines.join('\n')}\n]${restArg});`; + }); + + externals.delete(lang); + const imports = ["import '../highlight.dart';"]; + for (const ext of [...externals].sort()) { + imports.push(`import '${fileName(ext)}.dart';`); + } + + const header = +`// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering +`; + const body = +`${imports.join('\n')} + +/// Syntax grammar for \`${lang}\`. +/// +/// Import this library only when you need \`${lang}\` highlighting; unused +/// languages are dropped from the build. +abstract final class ${cls(lang)} { + /// The grammar for \`${lang}\`. + static final Grammar grammar = _g0; +} + +${decls.join('\n\n')} +`; + fs.writeFileSync(path.join(OUT_DIR, `${fileName(lang)}.dart`), `${header}\n${body}`); +} + +const names = Object.keys(languages); +for (const lang of names) generate(lang); + +// Convenience registry โ€” references EVERY grammar (defeats tree-shaking on +// purpose). For demos and tools only; production code should build its own map. +const sorted = [...names].sort(); +const importLines = sorted.map((n) => `import '${fileName(n)}.dart';`).join('\n'); +const exportLines = sorted.map((n) => `export '${fileName(n)}.dart';`).join('\n'); +const entries = []; +for (const n of sorted) entries.push(` '${n}': ${cls(n)}.grammar,`); +for (const [alias, target] of Object.entries(aliases).sort()) { + entries.push(` '${alias}': ${cls(target)}.grammar,`); +} +const allDart = +`// GENERATED CODE โ€” do not modify by hand. Regenerate with tool/highlight_codegen. +// ignore_for_file: lines_longer_than_80_chars, public_member_api_docs +// ignore_for_file: prefer_single_quotes, require_trailing_commas, directives_ordering + +import '../highlight.dart'; +${importLines} +${exportLines} + +/// Every bundled syntax grammar, keyed by language tag and common aliases. +/// +/// Referencing this map pulls in ALL grammars, so unused languages can no longer +/// be removed by tree-shaking โ€” use it for demos or tooling. Production code +/// should assemble a map with only the languages it needs. +final Map allHighlightLanguages = { +${entries.join('\n')} +}; +`; +fs.writeFileSync(path.join(OUT_DIR, 'all.dart'), allDart); + +console.log(`wrote ${names.length} language files + all.dart to lib/highlight/`); diff --git a/tool/highlight_codegen/dump.cjs b/tool/highlight_codegen/dump.cjs new file mode 100644 index 0000000..3239123 --- /dev/null +++ b/tool/highlight_codegen/dump.cjs @@ -0,0 +1,95 @@ +// Snapshot Prism grammars (the full dependency closure of the requested +// languages) into grammars.json for the Dart codegen. +const fs = require('fs'); +const Prism = require('prismjs'); +const loadLanguages = require('prismjs/components/'); + +const requested = require('./languages.json'); +loadLanguages(requested); + +// Canonical name per distinct grammar object (first key wins), plus aliases. +const canonical = new Map(); // object -> canonical name +const aliases = {}; // alias name -> canonical name +for (const name of Object.keys(Prism.languages)) { + const g = Prism.languages[name]; + if (!g || typeof g !== 'object' || Array.isArray(g)) continue; // skip fns + if (!canonical.has(g)) canonical.set(g, name); + else aliases[name] = canonical.get(g); +} + +function isRegExp(o) { return o instanceof RegExp; } +function isToken(o) { + return o && typeof o === 'object' && !Array.isArray(o) && !isRegExp(o) && + 'pattern' in o; +} +function isGrammar(o) { + return o && typeof o === 'object' && !Array.isArray(o) && !isRegExp(o) && + !('pattern' in o); +} + +let skippedFns = 0; + +// Build one language's id-hoisted grammar table. +function build(rootName) { + const root = Prism.languages[rootName]; + const ids = new Map(); + const defs = []; + + function grammarId(g) { + if (ids.has(g)) return ids.get(g); + const id = defs.length; + ids.set(g, id); + defs.push(null); + const entries = []; + let rest = null; + for (const key of Object.keys(g)) { + if (key === 'rest') { rest = grammarRef(g[key]); continue; } + const v = ser(g[key]); + if (v.t === 'skip') continue; + entries.push({ name: key, value: v }); + } + defs[id] = { entries, rest }; + return id; + } + + // A reference to a whole grammar (used by `inside` and `rest`). + function grammarRef(g) { + if (!isGrammar(g)) return null; + if (canonical.has(g)) return { langref: canonical.get(g) }; + return { ref: grammarId(g) }; + } + + function ser(node) { + if (isRegExp(node)) return { t: 're', s: node.source, f: node.flags }; + if (Array.isArray(node)) { + return { + t: 'arr', + items: node.map((x) => ser(x)).filter((x) => x.t !== 'skip'), + }; + } + if (isToken(node)) { + if (!isRegExp(node.pattern)) { skippedFns++; return { t: 'skip' }; } + return { + t: 'tok', s: node.pattern.source, f: node.pattern.flags, + lb: !!node.lookbehind, g: !!node.greedy, + alias: node.alias ?? null, + inside: grammarRef(node.inside), + }; + } + if (isGrammar(node)) return grammarRef(node) ?? { t: 'skip' }; + if (typeof node === 'function') { skippedFns++; return { t: 'skip' }; } + return { t: 'skip' }; // primitive/undefined hole + } + + grammarId(root); + return { rootName, defs }; +} + +const languages = {}; +for (const g of canonical.values()) languages[g] = build(g); + +fs.writeFileSync('grammars.json', JSON.stringify({ languages, aliases })); + +const count = Object.keys(languages).length; +console.log(`requested ${requested.length}; closure of ${count} languages`); +console.log(`aliases: ${Object.keys(aliases).length}; skipped fn/primitive: ${skippedFns}`); diff --git a/tool/highlight_codegen/languages.json b/tool/highlight_codegen/languages.json new file mode 100644 index 0000000..a1f657f --- /dev/null +++ b/tool/highlight_codegen/languages.json @@ -0,0 +1,64 @@ +[ + "markup", + "css", + "clike", + "javascript", + "typescript", + "jsx", + "tsx", + "coffeescript", + "json", + "json5", + "yaml", + "toml", + "ini", + "markdown", + "python", + "ruby", + "php", + "java", + "kotlin", + "scala", + "groovy", + "clojure", + "dart", + "go", + "rust", + "swift", + "objectivec", + "c", + "cpp", + "csharp", + "fsharp", + "haskell", + "elixir", + "erlang", + "elm", + "lua", + "perl", + "r", + "julia", + "ocaml", + "bash", + "powershell", + "batch", + "sql", + "graphql", + "docker", + "makefile", + "nginx", + "apacheconf", + "diff", + "git", + "regex", + "protobuf", + "wasm", + "solidity", + "scss", + "sass", + "less", + "latex", + "vim", + "http", + "handlebars" +] diff --git a/tool/highlight_codegen/package.json b/tool/highlight_codegen/package.json new file mode 100644 index 0000000..0995cfd --- /dev/null +++ b/tool/highlight_codegen/package.json @@ -0,0 +1,7 @@ +{ + "name": "prism_codegen", + "private": true, + "version": "0.0.0", + "type": "commonjs", + "dependencies": { "prismjs": "1.30.0" } +}