aegis-desktop 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/lib/deep-link.js +83 -0
- package/lib/local/engine.js +398 -29
- package/lib/local/tools.js +257 -13
- package/lib/quick-launcher.js +81 -0
- package/lib/settings.js +76 -0
- package/lib/sync/sessions.js +27 -0
- package/lib/window-state.js +63 -0
- package/main.js +971 -32
- package/package.json +8 -4
- package/preload.js +121 -0
- package/renderer/app.js +533 -16
- package/renderer/index.html +49 -0
- package/renderer/markdown.js +165 -0
- package/renderer/quick.css +126 -0
- package/renderer/quick.html +39 -0
- package/renderer/quick.js +188 -0
- package/renderer/style.css +341 -0
- package/renderer/vendor/NOTICE.md +20 -0
- package/renderer/vendor/aegis-highlight.js +135 -0
- package/renderer/vendor/marked.umd.js +80 -0
- package/renderer/vendor/purify.min.js +3 -0
- package/vendor/aegis.js +34 -4
package/renderer/style.css
CHANGED
|
@@ -43,6 +43,35 @@ body {
|
|
|
43
43
|
font: 14px/1.5 var(--font-display);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* ----------------------------------------------------------- update banner */
|
|
47
|
+
.update-banner {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 12px;
|
|
51
|
+
padding: 8px 16px;
|
|
52
|
+
background: var(--surface2);
|
|
53
|
+
border-bottom: 1px solid var(--border);
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
color: var(--text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Same author-stylesheet-beats-UA-stylesheet gotcha as .autonomous-controls
|
|
59
|
+
below: `display: flex` here outranks `[hidden] { display: none }` from the
|
|
60
|
+
UA sheet, so the `hidden` attribute was a no-op with no visual effect. That
|
|
61
|
+
left the banner permanently on screen from first paint — its initial
|
|
62
|
+
`hidden` in index.html did nothing — and made the "Later" button look dead,
|
|
63
|
+
since its dismissal only sets `.hidden = true`. This override restores both
|
|
64
|
+
the initial hidden state and every silent status in renderUpdateBanner. */
|
|
65
|
+
.update-banner[hidden] {
|
|
66
|
+
display: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.update-banner-actions {
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: 8px;
|
|
72
|
+
margin-left: auto;
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
/* --------------------------------------------------------------- top bar */
|
|
47
76
|
.topbar {
|
|
48
77
|
display: flex;
|
|
@@ -852,6 +881,26 @@ body.memory-open {
|
|
|
852
881
|
gap: 2px;
|
|
853
882
|
}
|
|
854
883
|
|
|
884
|
+
/* Extended-reasoning trace for a "work autonomously" turn (streamed as
|
|
885
|
+
`reasoning` deltas — see renderer/app.js ensureReasoningEl). Deliberation
|
|
886
|
+
shown above the answer: dimmer and smaller than body copy so it reads as
|
|
887
|
+
working-out rather than the reply itself. */
|
|
888
|
+
.reasoning {
|
|
889
|
+
white-space: pre-wrap;
|
|
890
|
+
word-break: break-word;
|
|
891
|
+
font-family: var(--font-mono);
|
|
892
|
+
font-size: 11px;
|
|
893
|
+
line-height: 1.5;
|
|
894
|
+
color: var(--text2);
|
|
895
|
+
opacity: 0.85;
|
|
896
|
+
max-height: 220px;
|
|
897
|
+
overflow-y: auto;
|
|
898
|
+
margin-bottom: 8px;
|
|
899
|
+
padding: 6px 8px;
|
|
900
|
+
border-left: 2px solid var(--border);
|
|
901
|
+
background: rgba(255, 255, 255, 0.02);
|
|
902
|
+
}
|
|
903
|
+
|
|
855
904
|
.meta {
|
|
856
905
|
font-size: 11px;
|
|
857
906
|
color: var(--text2);
|
|
@@ -1264,6 +1313,128 @@ label {
|
|
|
1264
1313
|
cursor: default;
|
|
1265
1314
|
}
|
|
1266
1315
|
|
|
1316
|
+
/* ------------------------------------------------- tool-call approval gate
|
|
1317
|
+
desktop/lib/local/engine.js requestApproval sends an `{ approval }` chunk
|
|
1318
|
+
for every first-time exec/writeFile/editFile call; renderApprovalCard()
|
|
1319
|
+
(renderer/app.js) renders it here, before the call runs. */
|
|
1320
|
+
.approval-slot {
|
|
1321
|
+
display: flex;
|
|
1322
|
+
flex-direction: column;
|
|
1323
|
+
gap: 8px;
|
|
1324
|
+
margin-bottom: 8px;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.approval-card {
|
|
1328
|
+
border: 1px solid var(--amber);
|
|
1329
|
+
border-radius: 8px;
|
|
1330
|
+
background: rgba(255, 145, 0, 0.08);
|
|
1331
|
+
padding: 8px 10px;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.approval-card.resolved {
|
|
1335
|
+
border-color: var(--border2);
|
|
1336
|
+
background: rgba(255, 255, 255, 0.03);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.approval-title {
|
|
1340
|
+
font-family: var(--font-mono);
|
|
1341
|
+
font-size: 11px;
|
|
1342
|
+
font-weight: 600;
|
|
1343
|
+
color: var(--amber);
|
|
1344
|
+
text-transform: uppercase;
|
|
1345
|
+
letter-spacing: 0.03em;
|
|
1346
|
+
margin-bottom: 4px;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.approval-card.resolved .approval-title {
|
|
1350
|
+
color: var(--text2);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.approval-summary {
|
|
1354
|
+
font-family: var(--font-mono);
|
|
1355
|
+
font-size: 12px;
|
|
1356
|
+
color: var(--text);
|
|
1357
|
+
word-break: break-word;
|
|
1358
|
+
margin-bottom: 6px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.approval-diff {
|
|
1362
|
+
font-family: var(--font-mono);
|
|
1363
|
+
font-size: 11px;
|
|
1364
|
+
line-height: 1.4;
|
|
1365
|
+
max-height: 220px;
|
|
1366
|
+
overflow-y: auto;
|
|
1367
|
+
overflow-x: auto;
|
|
1368
|
+
white-space: pre;
|
|
1369
|
+
background: var(--bg);
|
|
1370
|
+
border: 1px solid var(--border);
|
|
1371
|
+
border-radius: 6px;
|
|
1372
|
+
padding: 6px 8px;
|
|
1373
|
+
margin-bottom: 8px;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.diff-add {
|
|
1377
|
+
color: var(--green);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.diff-del {
|
|
1381
|
+
color: var(--red);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.diff-hunk {
|
|
1385
|
+
color: var(--purple);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.diff-file {
|
|
1389
|
+
color: var(--text2);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.diff-ctx {
|
|
1393
|
+
color: var(--text2);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
.approval-actions {
|
|
1397
|
+
display: flex;
|
|
1398
|
+
gap: 6px;
|
|
1399
|
+
flex-wrap: wrap;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.approval-btn {
|
|
1403
|
+
border-radius: 6px;
|
|
1404
|
+
padding: 4px 10px;
|
|
1405
|
+
font: inherit;
|
|
1406
|
+
font-size: 11px;
|
|
1407
|
+
cursor: pointer;
|
|
1408
|
+
background: transparent;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.approval-btn.allow {
|
|
1412
|
+
border: 1px solid var(--green);
|
|
1413
|
+
color: var(--green);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.approval-btn.allow-session {
|
|
1417
|
+
border: 1px solid var(--teal);
|
|
1418
|
+
color: var(--teal);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.approval-btn.deny {
|
|
1422
|
+
border: 1px solid var(--red);
|
|
1423
|
+
color: var(--red);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.approval-btn:disabled {
|
|
1427
|
+
opacity: 0.5;
|
|
1428
|
+
cursor: default;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.approval-decision {
|
|
1432
|
+
margin-top: 6px;
|
|
1433
|
+
font-family: var(--font-mono);
|
|
1434
|
+
font-size: 11px;
|
|
1435
|
+
color: var(--text2);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1267
1438
|
/* ------------------------------------------------------- settings pane */
|
|
1268
1439
|
.setting-row {
|
|
1269
1440
|
border: 1px solid var(--border);
|
|
@@ -1452,3 +1623,173 @@ label {
|
|
|
1452
1623
|
max-width: 460px;
|
|
1453
1624
|
line-height: 1.6;
|
|
1454
1625
|
}
|
|
1626
|
+
|
|
1627
|
+
/* ------------------------------------------------- rendered markdown (D?)
|
|
1628
|
+
renderer/markdown.js parses assistant text with marked, sanitizes with
|
|
1629
|
+
DOMPurify, then hands the sanitized HTML to .body/.flow-body via
|
|
1630
|
+
innerHTML. md-body opts those containers out of the plain-text
|
|
1631
|
+
`white-space: pre-wrap` (see .body/.flow-body above) so marked's own
|
|
1632
|
+
block elements (p/ul/pre/h1…) supply spacing instead of every inter-tag
|
|
1633
|
+
newline in the generated HTML rendering as a visible blank line. */
|
|
1634
|
+
.md-body {
|
|
1635
|
+
white-space: normal;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
.md-body p {
|
|
1639
|
+
margin: 0 0 8px;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
.md-body p:last-child {
|
|
1643
|
+
margin-bottom: 0;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
.md-body ul,
|
|
1647
|
+
.md-body ol {
|
|
1648
|
+
margin: 0 0 8px;
|
|
1649
|
+
padding-left: 1.4em;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
.md-body li {
|
|
1653
|
+
margin-bottom: 2px;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
.md-body h1,
|
|
1657
|
+
.md-body h2,
|
|
1658
|
+
.md-body h3,
|
|
1659
|
+
.md-body h4 {
|
|
1660
|
+
font-family: var(--font-display);
|
|
1661
|
+
line-height: 1.3;
|
|
1662
|
+
margin: 12px 0 6px;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
.md-body h1:first-child,
|
|
1666
|
+
.md-body h2:first-child,
|
|
1667
|
+
.md-body h3:first-child,
|
|
1668
|
+
.md-body h4:first-child {
|
|
1669
|
+
margin-top: 0;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
.md-body blockquote {
|
|
1673
|
+
margin: 0 0 8px;
|
|
1674
|
+
padding: 2px 10px;
|
|
1675
|
+
border-left: 2px solid var(--border2);
|
|
1676
|
+
color: var(--text2);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.md-body a {
|
|
1680
|
+
color: var(--teal);
|
|
1681
|
+
text-decoration: underline;
|
|
1682
|
+
text-underline-offset: 2px;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
.md-body table {
|
|
1686
|
+
border-collapse: collapse;
|
|
1687
|
+
margin: 0 0 8px;
|
|
1688
|
+
font-size: 12px;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.md-body th,
|
|
1692
|
+
.md-body td {
|
|
1693
|
+
border: 1px solid var(--border);
|
|
1694
|
+
padding: 4px 8px;
|
|
1695
|
+
text-align: left;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.md-body hr {
|
|
1699
|
+
border: none;
|
|
1700
|
+
border-top: 1px solid var(--border);
|
|
1701
|
+
margin: 10px 0;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/* Inline `code` spans, as opposed to fenced ```code blocks``` (below). */
|
|
1705
|
+
.md-body :not(pre) > code {
|
|
1706
|
+
font-family: var(--font-mono);
|
|
1707
|
+
font-size: 0.92em;
|
|
1708
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1709
|
+
border: 1px solid var(--border);
|
|
1710
|
+
border-radius: 4px;
|
|
1711
|
+
padding: 1px 5px;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/* Fenced code block, decorated by renderer/markdown.js decorateCodeBlock()
|
|
1715
|
+
with an AegisHighlight-tokenized <code> and a copy button. */
|
|
1716
|
+
.code-block {
|
|
1717
|
+
position: relative;
|
|
1718
|
+
margin: 0 0 8px;
|
|
1719
|
+
padding: 10px 12px;
|
|
1720
|
+
padding-top: 26px;
|
|
1721
|
+
background: var(--bg);
|
|
1722
|
+
border: 1px solid var(--border);
|
|
1723
|
+
border-radius: 8px;
|
|
1724
|
+
overflow-x: auto;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
.code-block code {
|
|
1728
|
+
font-family: var(--font-mono);
|
|
1729
|
+
font-size: 12px;
|
|
1730
|
+
line-height: 1.5;
|
|
1731
|
+
white-space: pre;
|
|
1732
|
+
color: var(--text);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.code-copy-btn {
|
|
1736
|
+
position: absolute;
|
|
1737
|
+
top: 6px;
|
|
1738
|
+
right: 6px;
|
|
1739
|
+
border: 1px solid var(--border2);
|
|
1740
|
+
border-radius: 6px;
|
|
1741
|
+
background: var(--surface);
|
|
1742
|
+
color: var(--text2);
|
|
1743
|
+
padding: 1px 8px;
|
|
1744
|
+
font-family: var(--font-mono);
|
|
1745
|
+
font-size: 10.5px;
|
|
1746
|
+
cursor: pointer;
|
|
1747
|
+
opacity: 0.7;
|
|
1748
|
+
transition: opacity 0.15s, color 0.15s, border-color 0.15s;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
.code-block:hover .code-copy-btn {
|
|
1752
|
+
opacity: 1;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
.code-copy-btn:hover {
|
|
1756
|
+
color: var(--teal);
|
|
1757
|
+
border-color: var(--teal);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
/* Syntax highlight tokens — vendor/aegis-highlight.js. Kept low-contrast
|
|
1761
|
+
against .code-block's dark background rather than a full editor theme. */
|
|
1762
|
+
.tok-comment {
|
|
1763
|
+
color: var(--text3);
|
|
1764
|
+
font-style: italic;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
.tok-string {
|
|
1768
|
+
color: var(--green);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
.tok-keyword {
|
|
1772
|
+
color: var(--purple);
|
|
1773
|
+
font-weight: 600;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
.tok-number {
|
|
1777
|
+
color: var(--amber);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
.tok-function {
|
|
1781
|
+
color: var(--teal);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
.tok-property,
|
|
1785
|
+
.tok-attr {
|
|
1786
|
+
color: var(--purple);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
.tok-tag {
|
|
1790
|
+
color: var(--teal);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
.tok-variable {
|
|
1794
|
+
color: var(--amber);
|
|
1795
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Vendored third-party files
|
|
2
|
+
|
|
3
|
+
These files are committed verbatim (no CDN, no network at runtime) and loaded
|
|
4
|
+
by `renderer/index.html` under the existing `script-src 'self'` CSP.
|
|
5
|
+
|
|
6
|
+
This directory (`renderer/vendor/`) is separate from `desktop/vendor/`, which
|
|
7
|
+
is a predist.mjs build-staging dir (copies of `client/aegis.js` +
|
|
8
|
+
`client/foreign-memory.js`) that the repo's `.githooks/pre-commit` guard
|
|
9
|
+
deliberately blocks from ever being committed — these files have no other
|
|
10
|
+
source of truth, so they live here instead.
|
|
11
|
+
|
|
12
|
+
- `marked.umd.js` — [marked](https://github.com/markedjs/marked) v18.0.12,
|
|
13
|
+
MIT / MPL-2.0 dual-licensed. Markdown → HTML parser. Exposes global `marked`.
|
|
14
|
+
- `purify.min.js` — [DOMPurify](https://github.com/cure53/DOMPurify) v3.4.15,
|
|
15
|
+
Apache-2.0 / MPL-2.0 dual-licensed. HTML sanitizer. Exposes global
|
|
16
|
+
`DOMPurify`.
|
|
17
|
+
- `aegis-highlight.js` — first-party (not third-party), see the file header.
|
|
18
|
+
|
|
19
|
+
Do not edit `marked.umd.js` / `purify.min.js` by hand — replace with a fresh
|
|
20
|
+
build from the matching npm package version instead.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Aegis Desktop — minimal self-contained syntax highlighter.
|
|
5
|
+
*
|
|
6
|
+
* First-party (not a third-party vendor drop like marked/DOMPurify): a small
|
|
7
|
+
* regex tokenizer covering the languages that actually show up in AEGIS chat
|
|
8
|
+
* output (JS/TS, Python, JSON, shell, CSS, HTML) without pulling in a full
|
|
9
|
+
* grammar engine like highlight.js/Prism. Unknown languages fall back to
|
|
10
|
+
* plain escaped text rather than guessing wrong colors.
|
|
11
|
+
*
|
|
12
|
+
* UMD-lite: works as a CommonJS module (tests) and as a plain <script> tag
|
|
13
|
+
* (renderer, CSP script-src 'self' — no eval, no Function()).
|
|
14
|
+
*/
|
|
15
|
+
(function (root, factory) {
|
|
16
|
+
const mod = factory();
|
|
17
|
+
if (typeof module === 'object' && module.exports) {
|
|
18
|
+
module.exports = mod;
|
|
19
|
+
} else {
|
|
20
|
+
root.AegisHighlight = mod;
|
|
21
|
+
}
|
|
22
|
+
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
23
|
+
function escapeHtml(s) {
|
|
24
|
+
return String(s)
|
|
25
|
+
.replace(/&/g, '&')
|
|
26
|
+
.replace(/</g, '<')
|
|
27
|
+
.replace(/>/g, '>');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build one alternation regex from an ordered list of [tokenClass, source]
|
|
32
|
+
* pairs (earlier entries win ties, e.g. comments before generic strings)
|
|
33
|
+
* and walk it, emitting escaped text for the gaps and
|
|
34
|
+
* `<span class="tok-X">` for matches. Empty matches advance by one char so
|
|
35
|
+
* a zero-width pattern can never spin the loop forever.
|
|
36
|
+
*/
|
|
37
|
+
function tokenize(code, patterns) {
|
|
38
|
+
const combined = new RegExp(patterns.map(([, src]) => '(' + src + ')').join('|'), 'g');
|
|
39
|
+
let out = '';
|
|
40
|
+
let last = 0;
|
|
41
|
+
let m;
|
|
42
|
+
while ((m = combined.exec(code))) {
|
|
43
|
+
if (m.index > last) out += escapeHtml(code.slice(last, m.index));
|
|
44
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
45
|
+
if (m[i + 1] !== undefined) {
|
|
46
|
+
out += `<span class="tok-${patterns[i][0]}">${escapeHtml(m[i + 1])}</span>`;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
last = combined.lastIndex;
|
|
51
|
+
if (m[0].length === 0) combined.lastIndex += 1;
|
|
52
|
+
}
|
|
53
|
+
out += escapeHtml(code.slice(last));
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NUM = String.raw`\b0x[\da-fA-F]+\b|\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b`;
|
|
58
|
+
const DQ_STR = String.raw`"(?:\\.|[^"\\\n])*"`;
|
|
59
|
+
const SQ_STR = String.raw`'(?:\\.|[^'\\\n])*'`;
|
|
60
|
+
const FUNC_CALL = String.raw`\b[A-Za-z_$][\w$]*(?=\s*\()`;
|
|
61
|
+
// Built from a real regex literal (not a hand-escaped string) so the
|
|
62
|
+
// backtick/backslash escaping is guaranteed correct — counting backslashes
|
|
63
|
+
// by hand across two rounds of escaping (JS string, then regex) is where
|
|
64
|
+
// this pattern kept breaking.
|
|
65
|
+
const TEMPLATE_STR = /`(?:\\.|[^`\\])*`/.source;
|
|
66
|
+
|
|
67
|
+
const GRAMMARS = {
|
|
68
|
+
js: [
|
|
69
|
+
['comment', String.raw`\/\/[^\n]*|\/\*[\s\S]*?\*\/`],
|
|
70
|
+
['string', `${TEMPLATE_STR}|${DQ_STR}|${SQ_STR}`],
|
|
71
|
+
['keyword', String.raw`\b(?:const|let|var|function|return|if|else|for|while|do|switch|case|default|break|continue|class|extends|new|this|super|import|export|from|as|async|await|try|catch|finally|throw|typeof|instanceof|in|of|null|undefined|true|false|void|yield|static|get|set|delete|interface|type|enum|implements|public|private|protected|readonly)\b`],
|
|
72
|
+
['function', FUNC_CALL],
|
|
73
|
+
['number', NUM],
|
|
74
|
+
],
|
|
75
|
+
python: [
|
|
76
|
+
['comment', String.raw`#[^\n]*`],
|
|
77
|
+
['string', String.raw`'''[\s\S]*?'''|"""[\s\S]*?"""|${DQ_STR}|${SQ_STR}`],
|
|
78
|
+
['keyword', String.raw`\b(?:def|return|if|elif|else|for|while|break|continue|class|import|from|as|try|except|finally|raise|with|pass|lambda|yield|global|nonlocal|assert|del|is|in|not|and|or|None|True|False|async|await|self)\b`],
|
|
79
|
+
['function', FUNC_CALL],
|
|
80
|
+
['number', NUM],
|
|
81
|
+
],
|
|
82
|
+
json: [
|
|
83
|
+
['string', String.raw`${DQ_STR}`],
|
|
84
|
+
['keyword', String.raw`\b(?:true|false|null)\b`],
|
|
85
|
+
['number', NUM],
|
|
86
|
+
],
|
|
87
|
+
bash: [
|
|
88
|
+
['comment', String.raw`#[^\n]*`],
|
|
89
|
+
['string', String.raw`${DQ_STR}|${SQ_STR}`],
|
|
90
|
+
['variable', String.raw`\$\{[^}]*\}|\$[A-Za-z_]\w*`],
|
|
91
|
+
['keyword', String.raw`\b(?:if|then|elif|else|fi|for|while|until|do|done|case|esac|function|return|export|local|echo|cd|exit|set|source|in)\b`],
|
|
92
|
+
],
|
|
93
|
+
css: [
|
|
94
|
+
['comment', String.raw`\/\*[\s\S]*?\*\/`],
|
|
95
|
+
['string', String.raw`${DQ_STR}|${SQ_STR}`],
|
|
96
|
+
['property', String.raw`[a-zA-Z-]+(?=\s*:)`],
|
|
97
|
+
['number', String.raw`\b\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms|deg)?\b`],
|
|
98
|
+
],
|
|
99
|
+
html: [
|
|
100
|
+
['comment', String.raw`<!--[\s\S]*?-->`],
|
|
101
|
+
['string', String.raw`${DQ_STR}|${SQ_STR}`],
|
|
102
|
+
['tag', String.raw`<\/?[a-zA-Z][\w-]*|\/?>`],
|
|
103
|
+
['attr', String.raw`\b[a-zA-Z-]+(?==)`],
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const ALIASES = {
|
|
108
|
+
js: 'js', javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js',
|
|
109
|
+
ts: 'js', typescript: 'js', tsx: 'js',
|
|
110
|
+
py: 'python', python: 'python', python3: 'python',
|
|
111
|
+
json: 'json', json5: 'json',
|
|
112
|
+
sh: 'bash', bash: 'bash', shell: 'bash', zsh: 'bash', console: 'bash',
|
|
113
|
+
css: 'css', scss: 'css', less: 'css',
|
|
114
|
+
html: 'html', xml: 'html', svg: 'html', htm: 'html',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Normalise a fenced-code-block lang string (e.g. "js" from ```js) to a
|
|
118
|
+
* known grammar key, or null when unsupported (caller should skip
|
|
119
|
+
* highlighting and just escape). */
|
|
120
|
+
function resolve(lang) {
|
|
121
|
+
if (!lang) return null;
|
|
122
|
+
const key = String(lang).trim().toLowerCase();
|
|
123
|
+
return ALIASES[key] || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Highlight `code` for `lang`. Always returns escaped-safe HTML — falls
|
|
127
|
+
* back to plain escaped text for an unrecognised or absent language. */
|
|
128
|
+
function highlight(code, lang) {
|
|
129
|
+
const grammar = resolve(lang);
|
|
130
|
+
if (!grammar) return escapeHtml(code);
|
|
131
|
+
return tokenize(code, GRAMMARS[grammar]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { highlight, resolve, escapeHtml };
|
|
135
|
+
});
|