feat(file-viewer): syntax highlighting for code files and chat code blocks
Nightly Build / build (push) Successful in 8s

Vendor highlight.js (core + python, javascript, typescript, json, yaml,
bash) and highlight the file viewer's text kind (computed once per load)
plus fenced blocks in renderMarkdown. Colors come from new --syn-* CSS
variables aliased to the existing palette, so dark mode follows.
This commit is contained in:
Daniele
2026-08-11 10:41:50 +01:00
parent 4d1b1e63be
commit 402c9ffe50
16 changed files with 240 additions and 3 deletions
+2
View File
@@ -40,6 +40,8 @@ A file explorer rooted at the project folder:
- Click a **folder** to navigate into it. - Click a **folder** to navigate into it.
- The listing **updates by itself**: if another member or the assistant creates, renames or deletes a file while you're looking at a folder, the change appears within a second — no refresh needed. - The listing **updates by itself**: if another member or the assistant creates, renames or deletes a file while you're looking at a folder, the change appears within a second — no refresh needed.
For **code files** — Python, JavaScript, TypeScript, JSON, YAML, shell scripts — the viewer colors the syntax (keywords, strings, comments…), so the structure of a script is easy to follow when reviewing it. The same coloring applies to code blocks inside chat messages.
For **PDF** files, the viewer shows the whole document as one continuous scroll — every page, in order, on phone, tablet and computer alike. A small toolbar on top gives zoom out / zoom in and tells you which page you are on (`3 / 12`). The text stays selectable and copiable where the PDF itself has real text. Pages are drawn as you reach them, so a long document opens quickly instead of making you wait for the last page. If you'd rather open it in another app, the **download** button in the header saves the original file. For **PDF** files, the viewer shows the whole document as one continuous scroll — every page, in order, on phone, tablet and computer alike. A small toolbar on top gives zoom out / zoom in and tells you which page you are on (`3 / 12`). The text stays selectable and copiable where the PDF itself has real text. Pages are drawn as you reach them, so a long document opens quickly instead of making you wait for the last page. If you'd rather open it in another app, the **download** button in the header saves the original file.
For **Markdown** files (`.md`), if you have write access the viewer has two tabs: For **Markdown** files (`.md`), if you have write access the viewer has two tabs:
+1 -1
View File
@@ -38,7 +38,7 @@ Two things worth knowing about membership:
There is no file explorer for shared folders — no grid of files, no upload button. The files live on the server, and members reach them through the assistant: There is no file explorer for shared folders — no grid of files, no upload button. The files live on the server, and members reach them through the assistant:
- **Ask the assistant** — "what's in the recipes folder?", "add this note to documents", "send me the manual for the boiler". The assistant knows which folders you belong to, can list their contents, open and search files, and — if you have read & write — create and edit them. - **Ask the assistant** — "what's in the recipes folder?", "add this note to documents", "send me the manual for the boiler". The assistant knows which folders you belong to, can list their contents, open and search files, and — if you have read & write — create and edit them.
- **Open a file** — when the assistant shows you a file from a shared folder, it opens in the usual file viewer (Markdown rendered, images, PDFs, text), exactly like any other file. You can read it there; editing in the viewer is available if you have read & write access. - **Open a file** — when the assistant shows you a file from a shared folder, it opens in the usual file viewer (Markdown rendered, images, PDFs, syntax-colored code, text), exactly like any other file. You can read it there; editing in the viewer is available if you have read & write access.
A practical consequence: if a member wants a file *from* a shared folder, the assistant is the way to get it — there is no download button on the folder itself. (An admin can of course reach the folder directly on the server, but members should not need to.) A practical consequence: if a member wants a file *from* a shared folder, the assistant is the way to get it — there is no download button on the folder itself. (An admin can of course reach the folder directly on the server, but members should not need to.)
+15
View File
@@ -2,6 +2,7 @@ import { html, nothing } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { keyed } from 'lit/directives/keyed.js'; import { keyed } from 'lit/directives/keyed.js';
import { LightElement, renderMarkdown } from '../../lib/base.js'; import { LightElement, renderMarkdown } from '../../lib/base.js';
import { codeLangForExt, highlightCode } from '../../lib/highlight.js';
import { fileWatcher } from '../../lib/file-watcher.js'; import { fileWatcher } from '../../lib/file-watcher.js';
import { t } from '../../lib/i18n.js'; import { t } from '../../lib/i18n.js';
import './pdf-view.js'; // registers <pdf-view>; pdf.js itself is imported lazily import './pdf-view.js'; // registers <pdf-view>; pdf.js itself is imported lazily
@@ -137,6 +138,7 @@ export class FileViewerBase extends LightElement {
_path: { state: true }, _path: { state: true },
_kind: { state: true }, _kind: { state: true },
_content: { state: true }, _content: { state: true },
_codeHtml: { state: true }, // highlighted HTML for code files (null = plain text)
_blobUrl: { state: true }, _blobUrl: { state: true },
_loading: { state: true }, _loading: { state: true },
_error: { state: true }, _error: { state: true },
@@ -163,6 +165,7 @@ export class FileViewerBase extends LightElement {
this._path = null; this._path = null;
this._kind = null; this._kind = null;
this._content = ''; this._content = '';
this._codeHtml = null;
this._blobUrl = null; this._blobUrl = null;
this._loading = false; this._loading = false;
this._error = null; this._error = null;
@@ -261,6 +264,7 @@ export class FileViewerBase extends LightElement {
this._path = null; this._path = null;
this._kind = null; this._kind = null;
this._content = ''; this._content = '';
this._codeHtml = null;
this._error = null; this._error = null;
this._compileError = null; this._compileError = null;
this._htmlMode = 'preview'; this._htmlMode = 'preview';
@@ -283,6 +287,7 @@ export class FileViewerBase extends LightElement {
this._path = path; this._path = path;
this._kind = kindFor(path); this._kind = kindFor(path);
this._content = ''; this._content = '';
this._codeHtml = null;
this._error = null; this._error = null;
this._compileError = null; this._compileError = null;
// Fresh load: drop any editor state from the previous file. // Fresh load: drop any editor state from the previous file.
@@ -315,6 +320,13 @@ export class FileViewerBase extends LightElement {
const res = await fetch(url); const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
this._content = await res.text(); this._content = await res.text();
// Syntax highlighting is computed once per load (not per render) and is
// best-effort: a highlight failure must never lose the file's content.
this._codeHtml = null;
if (this._kind === 'text') {
try { this._codeHtml = highlightCode(this._content, codeLangForExt(extOf(path))); }
catch { this._codeHtml = null; }
}
// Optimistic-locking version token + write flag (editable surface). // Optimistic-locking version token + write flag (editable surface).
this._etag = res.headers.get('ETag'); this._etag = res.headers.get('ETag');
this._canWrite = res.headers.get('X-Writable') === '1'; this._canWrite = res.headers.get('X-Writable') === '1';
@@ -788,6 +800,9 @@ export class FileViewerBase extends LightElement {
<pre class="fv-code"><code>${this._content}</code></pre> <pre class="fv-code"><code>${this._content}</code></pre>
`; `;
} }
if (this._codeHtml != null) {
return html`<pre class="fv-code"><code class="hljs">${unsafeHTML(this._codeHtml)}</code></pre>`;
}
return html`<pre class="fv-code"><code>${this._content}</code></pre>`; return html`<pre class="fv-code"><code>${this._content}</code></pre>`;
} }
} }
+73
View File
@@ -0,0 +1,73 @@
/* ── Syntax highlighting (highlight.js) ─────────────────────────────────────
Token colors for code rendered through web/lib/highlight.js: the file
viewer's code kind and fenced blocks in chat markdown. All colors come from
the --syn-* variables (variables.css), which track the light/dark palette.
No background here: the container (.fv-code, chat <pre>) already owns it. */
.hljs {
color: inherit;
background: transparent;
}
.hljs-comment,
.hljs-quote {
color: var(--syn-comment);
font-style: italic;
}
.hljs-keyword,
.hljs-literal,
.hljs-selector-tag {
color: var(--syn-keyword);
}
.hljs-string,
.hljs-regexp,
.hljs-template-string,
.hljs-addition {
color: var(--syn-string);
}
.hljs-number,
.hljs-symbol,
.hljs-bullet {
color: var(--syn-number);
}
.hljs-title,
.hljs-section {
color: var(--syn-title);
}
.hljs-built_in,
.hljs-type,
.hljs-title.class_ {
color: var(--syn-builtin);
}
.hljs-attr,
.hljs-attribute,
.hljs-variable,
.hljs-template-variable,
.hljs-name {
color: var(--syn-attr);
}
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-selector-class,
.hljs-selector-id {
color: var(--syn-meta);
}
/* Punctuation, operators and params stay on the base text color. */
.hljs-subst,
.hljs-params,
.hljs-punctuation,
.hljs-operator {
color: inherit;
}
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: bold; }
.hljs-link { text-decoration: underline; }
+11
View File
@@ -32,6 +32,17 @@
--radius-md: 12px; --radius-md: 12px;
--radius-lg: 16px; --radius-lg: 16px;
/* Syntax highlighting (consumed by hljs-theme.css). Aliases of the palette
above, so the dark block needs no copies — var() resolves at use time. */
--syn-comment: var(--tool-shell);
--syn-keyword: var(--accent-hover);
--syn-string: var(--tool-outline);
--syn-number: var(--tool-config);
--syn-title: var(--tool-edit);
--syn-builtin: var(--tool-list);
--syn-attr: var(--tool-read);
--syn-meta: var(--tool-search);
/* Sidebar — warm cream */ /* Sidebar — warm cream */
--sidebar-bg: #f6f0e6; --sidebar-bg: #f6f0e6;
--sidebar-hover: rgba(63, 52, 40, 0.06); --sidebar-hover: rgba(63, 52, 40, 0.06);
+1
View File
@@ -70,6 +70,7 @@
<link rel="stylesheet" href="css/projects/base.css" /> <link rel="stylesheet" href="css/projects/base.css" />
<link rel="stylesheet" href="css/projects/board.css" /> <link rel="stylesheet" href="css/projects/board.css" />
<link rel="stylesheet" href="css/file-viewer.css" /> <link rel="stylesheet" href="css/file-viewer.css" />
<link rel="stylesheet" href="css/hljs-theme.css" />
<!-- pdf.js TextLayer's DOM contract (selectable text over the rendered pages). --> <!-- pdf.js TextLayer's DOM contract (selectable text over the rendered pages). -->
<link rel="stylesheet" href="/vendor/pdf-text-layer.css" /> <link rel="stylesheet" href="/vendor/pdf-text-layer.css" />
+5 -1
View File
@@ -2,6 +2,7 @@ import { LitElement } from 'lit';
import { marked } from 'marked'; import { marked } from 'marked';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { t } from './i18n.js'; import { t } from './i18n.js';
import { highlightMarkdownCodeBlocks } from './highlight.js';
marked.use({ breaks: true, gfm: true }); marked.use({ breaks: true, gfm: true });
@@ -36,12 +37,15 @@ export function renderMarkdown(text) {
// `target` is not in DOMPurify's default attribute allow-list, so the // `target` is not in DOMPurify's default attribute allow-list, so the
// external-link hook above needs it whitelisted here to survive sanitization. // external-link hook above needs it whitelisted here to survive sanitization.
const html = DOMPurify.sanitize(marked.parse(text ?? ''), { ADD_ATTR: ['target'] }); const html = DOMPurify.sanitize(marked.parse(text ?? ''), { ADD_ATTR: ['target'] });
// Syntax-highlight fenced code blocks whose language we support (hljs escapes
// its own output; unknown fences stay plain).
const highlighted = highlightMarkdownCodeBlocks(html);
// Wrap fenced code blocks in .md-code-wrap so a copy button can float over // Wrap fenced code blocks in .md-code-wrap so a copy button can float over
// them on hover. `<pre>` reaches this point only from a marked code block — // them on hover. `<pre>` reaches this point only from a marked code block —
// a literal one in the source text is escaped by sanitize, so the string // a literal one in the source text is escaped by sanitize, so the string
// replace cannot wrap anything else. // replace cannot wrap anything else.
const btn = `<button type="button" class="md-code-copy" title="${t('chat.copy_code')}"><i class="bi bi-clipboard"></i></button>`; const btn = `<button type="button" class="md-code-copy" title="${t('chat.copy_code')}"><i class="bi bi-clipboard"></i></button>`;
return html.replaceAll('<pre>', `<div class="md-code-wrap">${btn}<pre>`) return highlighted.replaceAll('<pre>', `<div class="md-code-wrap">${btn}<pre>`)
.replaceAll('</pre>', '</pre></div>'); .replaceAll('</pre>', '</pre></div>');
} }
+72
View File
@@ -0,0 +1,72 @@
import hljs from '../vendor/hljs/core.min.js';
import DOMPurify from 'dompurify';
import python from '../vendor/hljs/languages/python.min.js';
import javascript from '../vendor/hljs/languages/javascript.min.js';
import typescript from '../vendor/hljs/languages/typescript.min.js';
import json from '../vendor/hljs/languages/json.min.js';
import yaml from '../vendor/hljs/languages/yaml.min.js';
import bash from '../vendor/hljs/languages/bash.min.js';
hljs.registerLanguage('python', python);
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('json', json);
hljs.registerLanguage('yaml', yaml);
hljs.registerLanguage('bash', bash);
// File extension → hljs language. Extensions not listed here render as plain
// text (never auto-detected: guessing wrong is worse than no colors).
const LANG_FOR_EXT = {
py: 'python',
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
ts: 'typescript', tsx: 'typescript',
json: 'json',
yml: 'yaml', yaml: 'yaml',
sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
};
// Markdown fence info string → hljs language (common aliases included).
const LANG_FOR_FENCE = {
py: 'python', python: 'python',
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript', javascript: 'javascript',
ts: 'typescript', tsx: 'typescript', typescript: 'typescript',
json: 'json',
yml: 'yaml', yaml: 'yaml',
sh: 'bash', bash: 'bash', shell: 'bash', zsh: 'bash',
};
export function codeLangForExt(ext) {
return LANG_FOR_EXT[ext] ?? null;
}
/**
* Highlight `code` as `lang`, returning sanitized HTML (hljs escapes the input
* itself; DOMPurify is belt-and-braces). Returns null when the language is not
* registered, so callers can fall back to plain text.
*/
export function highlightCode(code, lang) {
if (!lang || !hljs.getLanguage(lang)) return null;
return DOMPurify.sanitize(hljs.highlight(code, { language: lang }).value);
}
/**
* Post-process sanitized marked HTML: highlight every `<pre><code
* class="language-…">` whose fence maps to a registered language, in place.
* Unknown fences are left as the plain (already escaped) text marked emitted.
*/
export function highlightMarkdownCodeBlocks(htmlStr) {
if (!htmlStr.includes('language-')) return htmlStr;
const tpl = document.createElement('template');
tpl.innerHTML = htmlStr;
let changed = false;
for (const code of tpl.content.querySelectorAll('pre > code[class*="language-"]')) {
const fence = [...code.classList].find(c => c.startsWith('language-'))?.slice(9);
const lang = LANG_FOR_FENCE[fence];
const out = lang && highlightCode(code.textContent ?? '', lang);
if (!out) continue;
code.innerHTML = out;
code.classList.add('hljs');
changed = true;
}
return changed ? tpl.innerHTML : htmlStr;
}
+1
View File
@@ -46,6 +46,7 @@
<link rel="stylesheet" href="css/agent-tasks.css" /> <link rel="stylesheet" href="css/agent-tasks.css" />
<link rel="stylesheet" href="css/inbox-cards.css" /> <link rel="stylesheet" href="css/inbox-cards.css" />
<link rel="stylesheet" href="css/file-viewer.css" /> <link rel="stylesheet" href="css/file-viewer.css" />
<link rel="stylesheet" href="css/hljs-theme.css" />
<!-- pdf.js TextLayer's DOM contract (selectable text over the rendered pages). --> <!-- pdf.js TextLayer's DOM contract (selectable text over the rendered pages). -->
<link rel="stylesheet" href="/vendor/pdf-text-layer.css" /> <link rel="stylesheet" href="/vendor/pdf-text-layer.css" />
<link rel="stylesheet" href="css/mobile.css" /> <link rel="stylesheet" href="css/mobile.css" />
+10
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/highlight.js@11.11.1/es/languages/bash.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
function bash(e){const s=e.regex,t={},n={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{className:"variable",variants:[{begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},i=e.inherit(e.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),c={begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},o={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(o);const r={begin:/\$?\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]},l=e.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),m={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,keyword:["if","then","else","elif","fi","time","for","while","until","in","do","done","case","esac","coproc","function","select"],literal:["true","false"],built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","sudo","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]},contains:[l,e.SHEBANG(),m,r,i,c,{match:/(\/[a-z._-]+)+/},o,{match:/\\"/},{className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}export{bash as default};
//# sourceMappingURL=/sm/de51d06e654380eeba27e164aa3184c082d7ebd41695db22b9cadc02d289a657.map
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.37.0.
* Original file: /npm/highlight.js@11.11.1/es/languages/json.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
function json(e){const a=["true","false","null"],n={scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",aliases:["jsonc"],keywords:{literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}}export{json as default};
//# sourceMappingURL=/sm/cc8c22863ca08ad2642e2c51559f66774c360ac66c3780814484c6d4373e04c3.map
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/highlight.js@11.11.1/es/languages/python.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
function python(e){const n=e.regex,a=/[\p{XID_Start}_]\p{XID_Continue}*/u,t=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={$pattern:/[A-Za-z]\w+|__\w+__/,keyword:t,built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],literal:["__debug__","Ellipsis","False","None","NotImplemented","True"],type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"]},s={className:"meta",begin:/^(>>>|\.\.\.) /},r={className:"subst",begin:/\{/,end:/\}/,keywords:i,illegal:/#/},l={begin:/\{\{/,relevance:0},o={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,s],relevance:10},{begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,s],relevance:10},{begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,s,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,s,l,r]},{begin:/([uU]|[rR])'/,end:/'/,relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,l,r]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},b="[0-9](_?[0-9])*",c=`(\\b(${b}))?\\.(${b})|\\b(${b})\\.`,d=`\\b|${t.join("|")}`,p={className:"number",relevance:0,variants:[{begin:`(\\b(${b})|(${c}))[eE][+-]?(${b})[jJ]?(?=${d})`},{begin:`(${c})[jJ]?`},{begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${d})`},{begin:`\\b0[bB](_?[01])+[lL]?(?=${d})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${d})`},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${d})`},{begin:`\\b(${b})[jJ](?=${d})`}]},g={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i,contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",s,p,o,e.HASH_COMMENT_MODE]}]};return r.contains=[o,p,s],{name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i,illegal:/(<\/|\?)|=>/,contains:[s,p,{scope:"variable.language",match:/\bself\b/},{beginKeywords:"if",relevance:0},{match:/\bor\b/,scope:"keyword"},o,g,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,a],scope:{1:"keyword",3:"title.function"},contains:[m]},{variants:[{match:[/\bclass/,/\s+/,a,/\s*/,/\(\s*/,a,/\s*\)/]},{match:[/\bclass/,/\s+/,a]}],scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[p,m,o]}]}}export{python as default};
//# sourceMappingURL=/sm/5a8c1b91a61b978b4dd40a28193492db6e070459b84d0d6c39038e5addf91c28.map
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
/**
* Minified by jsDelivr using Terser v5.39.0.
* Original file: /npm/highlight.js@11.11.1/es/languages/yaml.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
function yaml(e){const n="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",s={className:"string",relevance:0,variants:[{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(s,{variants:[{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},t={end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},c={begin:/\{/,end:/\}/,contains:[t],illegal:"\\n",relevance:0},b={begin:"\\[",end:"\\]",contains:[t],illegal:"\\n",relevance:0},g=[{className:"attr",variants:[{begin:/[\w*@][\w*@ :()\./-]*:(?=[ \t]|$)/},{begin:/"[\w*@][\w*@ :()\./-]*":(?=[ \t]|$)/},{begin:/'[\w*@][\w*@ :()\./-]*':(?=[ \t]|$)/}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type",begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},l,{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},c,b,{className:"string",relevance:0,begin:/'/,end:/'/,contains:[{match:/''/,scope:"char.escape",relevance:0}]},s],r=[...g];return r.pop(),r.push(i),t.contains=r,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:g}}export{yaml as default};
//# sourceMappingURL=/sm/5b3961de0ff3696c1cfff7a4550ffc4f9694605df34cfd6da27a118f3dd5ea41.map