Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions assets/katex/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2013-2020 Khan Academy and other contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
17 changes: 17 additions & 0 deletions assets/katex/VERSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Vendored KaTeX 0.18.1

**Version:** 0.18.1
**Source:** npm registry tarball (`npm pack katex@0.18.1`), NOT wired in as an npm dependency -- there is no committed package-lock, and a floating version could silently change the rendering otherwise.
**License:** MIT (Khan Academy and contributors) -- see `LICENSE` in this folder, taken unmodified from the tarball.

## Vendored files
The runtime artifacts from the tarball's `dist/` plus `LICENSE`, 23 files in total:
- `katex.min.js` (KaTeX bundle, UMD, sets `window.katex`)
- `katex.min.css` (references `fonts/*.woff2` relatively; woff/ttf are not shipped -- every `@font-face` lists `woff2` first, modern VS Code webviews need no fallback)
- `fonts/*.woff2` (20 files, one per font variant)
- `LICENSE` (MIT, taken unmodified from the tarball)

`build/copy-assets.js` removes `out/katex/` first (no leftovers across version updates) and then copies exactly these 23 files there (part of `npm run compile`), so the MIT attribution ships with every deploy. Only this `VERSION.md` (update notes for developers) stays exclusively here in `assets/`.

## Update instructions
Run `npm pack katex@<new-version>` in a temp folder, unpack the tarball, and copy `dist/katex.min.js`, `dist/katex.min.css`, `dist/fonts/*.woff2` as well as `LICENSE` 1:1 over the files in this folder. Then run `npm run compile` and the `test:webview-syntax` + `test:math-segments` gates, and update the version number at the top of this file.
Binary file added assets/katex/fonts/KaTeX_AMS-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Caligraphic-Bold.woff2
Binary file not shown.
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Fraktur-Bold.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Fraktur-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Main-Bold.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Main-BoldItalic.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Main-Italic.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Main-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Math-BoldItalic.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Math-Italic.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_SansSerif-Bold.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_SansSerif-Italic.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_SansSerif-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Script-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Size1-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Size2-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Size3-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Size4-Regular.woff2
Binary file not shown.
Binary file added assets/katex/fonts/KaTeX_Typewriter-Regular.woff2
Binary file not shown.
1 change: 1 addition & 0 deletions assets/katex/katex.min.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/katex/katex.min.js

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions build/check-webview-syntax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node
'use strict';

// Committed version of a check we used to run by hand before shipping the KaTeX
// rendering feature: script.ts/ui.ts are giant TypeScript template literals producing
// the webview's HTML/JS as plain strings, so tsc happily compiles a typo like a stray unescaped
// backtick or "${" -- it only breaks once that string reaches an actual browser. This
// script proves the emitted <script> content still parses, and that ui.ts actually
// references the vendored KaTeX assets. Node builtins only, run via
// `npm run test:webview-syntax` (which compiles first).

const path = require('path');

function fail(message) {
console.error('FAIL: ' + message);
process.exit(1);
}

let getScript;
let getHtml;
try {
getScript = require(path.join('..', 'out', 'script.js')).default;
getHtml = require(path.join('..', 'out', 'ui.js')).default;
} catch (e) {
fail('could not load out/script.js / out/ui.js (did npm run compile run first?): ' + e.message);
}

if (typeof getScript !== 'function') {
fail('out/script.js has no default export function (getScript)');
}
if (typeof getHtml !== 'function') {
fail('out/ui.js has no default export function (getHtml)');
}

// ── 1) out/script.js: syntax-proof the <script> content getScript() returns ──
const scriptHtml = getScript(false);

const scriptBlockRe = /<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/g;
const scriptBlocks = [...scriptHtml.matchAll(scriptBlockRe)];
if (scriptBlocks.length === 0) {
fail('getScript(false) did not contain a <script>...</script> block to check');
}
for (let i = 0; i < scriptBlocks.length; i++) {
const body = scriptBlocks[i][1];
try {
// Parses (doesn't execute) -- a syntax proof only, same as the "new Function"
// smoke test this used to be run as a manual step. Browser globals (document,
// window, vscode, katex, ...) aren't defined here and don't need to be: we're
// not calling the resulting function, just confirming it compiles.
new Function(body);
} catch (e) {
fail('<script> block ' + (i + 1) + '/' + scriptBlocks.length + ' failed to parse: ' + e.message);
}
}

// ── 2) out/ui.js: getHtml() must actually reference the vendored KaTeX assets ──
const html = getHtml(false, undefined, undefined, undefined, 'check-webview-syntax', '0.0.0', 'vscode-resource://check/out/katex');
if (!html.includes('katex.min.css')) {
fail('getHtml(...) output does not reference katex.min.css');
}
if (!html.includes('katex.min.js')) {
fail('getHtml(...) output does not reference katex.min.js');
}

// -- 3) The code-block restore loop in parseSimpleMarkdown must run through the
// spliced restoreCodeBlockPlaceholders(html, codeBlockPlaceholders), never again
// through a direct html.replace(placeholder, str) -- String.replace interprets
// "$&"/"$`"/"$'"/"$$" in the replacement string as substitution patterns and thereby
// corrupts any code block that contains one of these sequences --
if (!scriptHtml.includes('function restoreCodeBlockPlaceholders')) {
fail('getScript(...) output does not contain the spliced restoreCodeBlockPlaceholders (markdown-restore.ts)');
}
// Discriminating needle: "restoreCodeBlockPlaceholders(html, codeBlockPlaceholders)" alone
// would be a tautology -- the string is already present in the spliced function head
// ("function restoreCodeBlockPlaceholders(html, codeBlockPlaceholders) {") and would thus
// still PASS even if the call site in parseSimpleMarkdown were removed. "html = " in front
// of it only occurs in the emit at the real call site.
if (!scriptHtml.includes('html = restoreCodeBlockPlaceholders(')) {
fail('getScript(...) output does not call restoreCodeBlockPlaceholders(...) from the code-block restore loop');
}
if (/html\.replace\(placeholder,\s*codeBlockPlaceholders\[i\]\)/.test(scriptHtml)) {
fail('getScript(...) output still contains the unsafe html.replace(placeholder, codeBlockPlaceholders[i]) string-replacement regression');
}

console.log('PASS: getScript() <script> content parses (' + scriptBlocks.length + ' block(s), ' + scriptHtml.length + ' chars), getHtml() references katex.min.css + katex.min.js, and the restoreCodeBlockPlaceholders splice is present and the code-block restore loop is safe');
process.exit(0);
30 changes: 30 additions & 0 deletions build/copy-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
'use strict';

// Second half of the "compile" npm script: tsc only handles src/**/*.ts, so
// anything the webview loads via asWebviewUri (the vendored KaTeX assets, see
// assets/katex/VERSION.md) needs an explicit copy into out/ -- tsc's outDir never sees
// non-.ts files. Node builtins only, no new dependency for a two-file build step.
//
// Copies katex.min.js + katex.min.css + LICENSE + 20 fonts/*.woff2 (23 files total) --
// LICENSE travels into out/ too so the MIT attribution ships with whatever gets deployed
// from there; only VERSION.md (dev-facing update notes) stays source-tree-only.
//
// out/katex/ is removed first (recursive, force) so a future KaTeX version that drops a
// file (e.g. a retired font) can't leave a stale leftover behind in the build output.

const fs = require('fs');
const path = require('path');

const assetsDir = path.join(__dirname, '..', 'assets', 'katex');
const outDir = path.join(__dirname, '..', 'out', 'katex');

fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(outDir, { recursive: true });
fs.cpSync(path.join(assetsDir, 'katex.min.js'), path.join(outDir, 'katex.min.js'));
fs.cpSync(path.join(assetsDir, 'katex.min.css'), path.join(outDir, 'katex.min.css'));
fs.cpSync(path.join(assetsDir, 'LICENSE'), path.join(outDir, 'LICENSE'));
fs.cpSync(path.join(assetsDir, 'fonts'), path.join(outDir, 'fonts'), { recursive: true });

const fontCount = fs.readdirSync(path.join(outDir, 'fonts')).length;
console.log('copy-assets: out/katex/ <- katex.min.js, katex.min.css, LICENSE, fonts/ (' + fontCount + ' font files)');
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,20 +208,28 @@
"type": "boolean",
"default": false,
"description": "Enable the local router to convert OpenAI format to Anthropic format. Required for providers that use OpenAI-compatible APIs."
},
"claudeCodeChat.ui.renderMath": {
"type": "boolean",
"default": true,
"description": "Render LaTeX/TeX math in chat messages ($...$, $$...$$, \\(...\\), \\[...\\]) using the bundled KaTeX."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"compile": "tsc -p ./ && node build/copy-assets.js",
"watch": "node build/copy-assets.js && tsc -watch -p ./",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test",
"test:downloader": "npm run compile && mocha --ui tdd \"out/test/downloader*.test.js\" --reporter spec --timeout 360000",
"test:downloader:unit": "npm run compile && mocha --ui tdd out/test/downloader.test.js --reporter spec",
"test:models": "npm run compile && mocha --ui tdd out/test/model-updater.test.js --reporter spec"
"test:models": "npm run compile && mocha --ui tdd out/test/model-updater.test.js --reporter spec",
"test:math-segments": "npm run compile && mocha --ui tdd out/test/math-segments.test.js --reporter spec",
"test:markdown-restore": "npm run compile && mocha --ui tdd out/test/markdown-restore.test.js --reporter spec",
"test:webview-syntax": "npm run compile && node build/check-webview-syntax.js"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
Expand Down
13 changes: 9 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ class ClaudeChatProvider {
const iconPath = vscode.Uri.joinPath(this._extensionUri, 'icon-bubble.png');
this._panel.iconPath = iconPath;

this._panel.webview.html = this._getHtmlForWebview();
this._panel.webview.html = this._getHtmlForWebview(this._panel.webview);

this._panel.onDidDispose(() => this.dispose(), null, this._disposables);

Expand Down Expand Up @@ -706,7 +706,7 @@ class ClaudeChatProvider {

this._webview = webview;
this._webviewView = webviewView;
this._webview.html = this._getHtmlForWebview();
this._webview.html = this._getHtmlForWebview(this._webview);

this._setupWebviewMessageHandler(this._webview);
this._initializePermissions();
Expand Down Expand Up @@ -3383,8 +3383,12 @@ class ClaudeChatProvider {
}
}

private _getHtmlForWebview(): string {
return getHtml(vscode.env?.isTelemetryEnabled, OPENCREDITS_API_URL, OPENCREDITS_WEB_URL, OPENCREDITS_PUBLISHABLE_KEY, vscode.env?.appName, this._context?.extension?.packageJSON?.version);
private _getHtmlForWebview(webview: vscode.Webview): string {
// out/katex/ (copy-assets.js, see assets/katex/VERSION.md) served through
// the webview's own resource URI scheme -- localResourceRoots already covers the
// whole extension folder at both call sites above.
const katexBaseUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'out', 'katex')).toString();
return getHtml(vscode.env?.isTelemetryEnabled, OPENCREDITS_API_URL, OPENCREDITS_WEB_URL, OPENCREDITS_PUBLISHABLE_KEY, vscode.env?.appName, this._context?.extension?.packageJSON?.version, katexBaseUri);
}

private _sendCurrentSettings(): void {
Expand All @@ -3400,6 +3404,7 @@ class ClaudeChatProvider {
'executable.path': config.get<string>('executable.path', ''),
'environment.variables': config.get<Record<string, string>>('environment.variables', {}),
'environment.disabled': config.get<boolean>('environment.disabled', false),
'ui.renderMath': config.get<boolean>('ui.renderMath', true),
'isOpenCredits': this._isOpenCredits()
};

Expand Down
20 changes: 20 additions & 0 deletions src/markdown-restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Placeholder back-substitution for code blocks in parseSimpleMarkdown (found during
// review). String.replace(placeholder, value) interprets "$&"/"$`"/"$'"/"$$" in the
// replacement string as substitution patterns -- a code block whose (already-escaped)
// content happens to contain such a sequence (e.g. shell code with "$'...'") corrupts
// the surrounding HTML instead of appearing unchanged. restoreMathSegments
// (math-script.ts) has had this fix from the start -- this function brings the code-block
// restore loop up to the same standard (function replacement instead of string
// replacement). script.ts splices only the compiled function text into the page via
// .toString() (same pattern as restoreMathSegments in math-script.ts) -- that's why this
// function must stay self-contained: no module-level symbol, no import, no helper
// function outside the body.
export function restoreCodeBlockPlaceholders(html: string, codeBlockPlaceholders: string[]): string {
for (let i = 0; i < codeBlockPlaceholders.length; i++) {
const placeholder = '__CODEBLOCK_' + i + '__';
const value = codeBlockPlaceholders[i];
// Function replacement, NEVER a string directly as the 2nd argument (see comment above).
html = html.replace(placeholder, function () { return value; });
}
return html;
}
83 changes: 83 additions & 0 deletions src/math-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { findMathSegments } from './math-segments';

// Webview-side LaTeX rendering (upstream #171), injected into script.ts's
// getScript() template the same way getSkillsScript()/getPluginsScript() are (see
// plugins-script.ts). Two different things happen below and they must not be confused:
//
// 1. findMathSegments.toString() is a REAL, host-side template interpolation (like
// getSkillsScript()/getPluginsScript() themselves): it runs in Node when getMathScript()
// is called, and splices that function's own *compiled* source into the returned
// string. math-segments.ts must stay fully self-contained for exactly this reason --
// only its own text crosses into the browser, not the rest of that module.
// 2. Everything else below (renderMathHtml, extractMathSegments, restoreMathSegments) is
// plain webview source written directly in this template literal. None of it happens to
// need a client-side "${...}" or a backtick, so nothing here needs the "\${"/"\`"
// escaping script.ts's own template literal requires elsewhere -- but if you add code
// that does, escape it the same way (see script.ts's parseSimpleMarkdown for examples).
const getMathScript = () => `
// ─── Math (KaTeX) ───
${findMathSegments.toString()}

// Fallback text for renderMathHtml: wraps the escaped source in $ / $$ (display
// uses the doubled form) so the message still shows it used to be a formula,
// instead of silently dropping to bare text indistinguishable from surrounding
// prose. Note this normalizes to $/$$ regardless of which delimiter the original
// segment used -- a \(...\) or \[...\] segment falls back to $ / $$ too, not its
// own backslash delimiters.
function mathFallbackText(tex, display) {
var delim = display ? '$$' : '$';
return delim + escapeHtml(tex) + delim;
}

// katex.min.js (loaded via a synchronous <script> before this one, see ui.ts)
// exposes window.katex. Falls back to mathFallbackText instead of dropping the
// message if the library is missing (e.g. out/katex/ didn't survive a deploy) or
// throws something throwOnError:false doesn't already catch.
function renderMathHtml(tex, display) {
if (typeof katex === 'undefined' || !katex || typeof katex.renderToString !== 'function') {
return mathFallbackText(tex, display);
}
try {
return katex.renderToString(tex, {
displayMode: display,
throwOnError: false,
trust: false,
strict: 'ignore'
});
} catch (e) {
return mathFallbackText(tex, display);
}
}

// Extraction half of the parseSimpleMarkdown hook: cuts every math segment out of
// the raw text and replaces it with a nonce-scoped placeholder, mirroring the
// __CODEBLOCK_N__ placeholders already used for fenced code. Walking segments
// back-to-front keeps earlier (start, end) offsets valid while later ones are
// spliced out of the string.
function extractMathSegments(text) {
var nonce = Math.random().toString(36).slice(2);
var segments = findMathSegments(text);
var placeholders = [];
for (var i = segments.length - 1; i >= 0; i--) {
var seg = segments[i];
var placeholder = '__CCCMATH_' + nonce + '_' + i + '__';
placeholders.push({ placeholder: placeholder, html: renderMathHtml(seg.tex, seg.display) });
text = text.slice(0, seg.start) + placeholder + text.slice(seg.end);
}
return { text: text, placeholders: placeholders };
}

// Restore half: uses function-replacement, never a plain string as the 2nd
// argument -- String.replace treats "$&"/"$\`"/"$'" in a string replacement as
// substitution patterns, and KaTeX's own HTML output can easily contain a "$"
// right before such a character.
function restoreMathSegments(html, placeholders) {
for (var i = 0; i < placeholders.length; i++) {
var mathHtml = placeholders[i].html;
html = html.replace(placeholders[i].placeholder, function() { return mathHtml; });
}
return html;
}
`;

export default getMathScript;
Loading