mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): render CJK emphasis in HTML export (#11168)
This commit is contained in:
parent
fbc456d827
commit
26293e74f6
7 changed files with 108 additions and 30 deletions
|
|
@ -15,6 +15,7 @@
|
|||
- Fixed compaction and branch summaries failing with "generation hit the token cap" when reasoning used most of the fixed summary output limit. Summaries now use the model's output limit, and `compaction.reserveTokens` only controls when compaction starts ([#9512](https://github.com/earendil-works/pi/issues/9512), [#9075](https://github.com/earendil-works/pi/issues/9075)).
|
||||
- Fixed `models.schema.json` missing descriptions for provider and model fields, so editors showed no hints for them ([#10790](https://github.com/earendil-works/pi/issues/10790))
|
||||
- Fixed `pi update` failing with "Managed install marker is missing or invalid" when `managed-install.json` starts with a UTF-8 BOM, which Windows PowerShell 5.1 adds when it rewrites the file.
|
||||
- Fixed HTML export showing literal asterisks for Markdown bold and italic next to CJK punctuation ([#3353](https://github.com/earendil-works/pi/issues/3353), [#10154](https://github.com/earendil-works/pi/issues/10154))
|
||||
|
||||
## [1.3.0] - 2026-10-15
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ function generateHtml(sessionData: SessionData, themeName?: string): string {
|
|||
const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8");
|
||||
const templateJs = readFileSync(join(templateDir, "template.js"), "utf-8");
|
||||
const markedJs = readFileSync(join(templateDir, "vendor", "marked.min.js"), "utf-8");
|
||||
const markedCjkFriendlyJs = readFileSync(join(templateDir, "vendor", "marked-cjk-friendly.min.js"), "utf-8");
|
||||
const hljsJs = readFileSync(join(templateDir, "vendor", "highlight.min.js"), "utf-8");
|
||||
|
||||
const themeVars = generateThemeVars(themeName);
|
||||
|
|
@ -171,6 +172,7 @@ function generateHtml(sessionData: SessionData, themeName?: string): string {
|
|||
.replace("{{JS}}", templateJs)
|
||||
.replace("{{SESSION_DATA}}", sessionDataBase64)
|
||||
.replace("{{MARKED_JS}}", markedJs)
|
||||
.replace("{{MARKED_CJK_FRIENDLY_JS}}", markedCjkFriendlyJs)
|
||||
.replace("{{HIGHLIGHT_JS}}", hljsJs);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
|
||||
<!-- Vendored libraries -->
|
||||
<script>{{MARKED_JS}}</script>
|
||||
<script>{{MARKED_CJK_FRIENDLY_JS}}</script>
|
||||
|
||||
<!-- highlight.js -->
|
||||
<script>{{HIGHLIGHT_JS}}</script>
|
||||
|
|
|
|||
|
|
@ -1614,7 +1614,8 @@
|
|||
text: match[2],
|
||||
tokens: this.lexer.inlineTokens(match[2])
|
||||
};
|
||||
}
|
||||
},
|
||||
emStrong: markedCjkFriendly().tokenizer.emStrong
|
||||
},
|
||||
renderer: {
|
||||
// Sanitize link URLs with a scheme allow-list. Browsers strip C0
|
||||
|
|
|
|||
6
packages/coding-agent/src/core/export-html/vendor/marked-cjk-friendly.min.js
vendored
Normal file
6
packages/coding-agent/src/core/export-html/vendor/marked-cjk-friendly.min.js
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* marked-cjk-friendly v0.1.2
|
||||
* Copyright (c) 2024 Tatsunori Uchino, and authors and contributors of original packages (MIT License)
|
||||
* https://github.com/tats-u/markdown-cjk-friendly/tree/main/packages/marked-cjk-friendly
|
||||
*/
|
||||
(function(){const d="\\u1100-\\u11ff\\u20a9\\u2329-\\u232a\\u2630-\\u2637\\u268a-\\u268f\\u2e80-\\u2e99\\u2e9b-\\u2ef3\\u2f00-\\u2fd5\\u2ff0-\\u303e\\u3041-\\u3096\\u3099-\\u30ff\\u3105-\\u312f\\u3131-\\u318e\\u3190-\\u31e5\\u31ef-\\u321e\\u3220-\\u3247\\u3250-\\ua48c\\ua490-\\ua4c6\\ua960-\\ua97c\\uac00-\\ud7a3\\ud7b0-\\ud7c6\\ud7cb-\\ud7fb\\uf900-\\ufaff\\ufe10-\\ufe19\\ufe30-\\ufe52\\ufe54-\\ufe66\\ufe68-\\ufe6b\\uff01-\\uffbe\\uffc2-\\uffc7\\uffca-\\uffcf\\uffd2-\\uffd7\\uffda-\\uffdc\\uffe0-\\uffe6\\uffe8-\\uffee\\u{16fe0}-\\u{16fe4}\\u{16ff0}-\\u{16ff6}\\u{17000}-\\u{18cda}\\u{18cff}-\\u{18d20}\\u{18d80}-\\u{18df2}\\u{18e00}-\\u{19191}\\u{191a0}-\\u{191d2}\\u{1aff0}-\\u{1aff3}\\u{1aff5}-\\u{1affb}\\u{1affd}-\\u{1affe}\\u{1b000}-\\u{1b128}\\u{1b132}\\u{1b150}-\\u{1b152}\\u{1b155}\\u{1b164}-\\u{1b168}\\u{1b170}-\\u{1b2fb}\\u{1d300}-\\u{1d356}\\u{1d360}-\\u{1d376}\\u{1f1ae}\\u{1f200}\\u{1f202}\\u{1f210}-\\u{1f219}\\u{1f21b}-\\u{1f22e}\\u{1f230}-\\u{1f231}\\u{1f237}\\u{1f23b}\\u{1f240}-\\u{1f248}\\u{1f260}-\\u{1f265}\\u{1f7da}\\u{20000}-\\u{3fffd}",k=new RegExp(`[${d}]`,"u"),y=new RegExp(`^((?![*_])[\\s\\p{P}\\p{S}${d}])`,"u");function P(e,t,n){return new RegExp(`^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)${e}(\\*+)(?=[\\s]|$)|${n}(\\*+)(?!\\*)(?=${t}|$)|(?!\\*)${t}(\\*+)(?=${n})|[\\s](\\*+)(?!\\*)(?=${e})|(?!\\*)${e}(\\*+)(?!\\*)(?=${e})|${n}(\\*+)(?=${n})`,"gu")}function B(e,t,n){return new RegExp(`^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)${e}(_+)(?=[\\s]|$)|${n}(_+)(?!_)(?=${t}|$)|(?!_)${t}(_+)(?=${n})|[\\s](_+)(?!_)(?=${e})|(?!_)${e}(_+)(?!_)(?=${e})`,"gu")}function I(e,t,n){return new RegExp(`^[^~]+(?=[^~])|(?!~)${e}(~~?)(?=[\\s]|$)|${n}(~~?)(?!~)(?=${t}|$)|(?!~)${t}(~~?)(?=${n})|[\\s](~~?)(?!~)(?=${e})|(?!~)${e}(~~?)(?!~)(?=${e})|${n}(~~?)(?=${n})`,"gu")}const h=`[\\p{P}\\p{S}${d}]`,D=`[\\s\\p{P}\\p{S}${d}]`,p=`[^\\s\\p{P}\\p{S}${d}]`,G=`(?![*_])[\\p{P}\\p{S}${d}]`,N=`(?![*_])[\\s\\p{P}\\p{S}${d}]`,U=`(?:[^\\s\\p{P}\\p{S}${d}]|[*_])`,F=`(?!~)[\\p{P}\\p{S}${d}]`,O=`(?!~)[\\s\\p{P}\\p{S}${d}]`,z=`(?:[^\\s\\p{P}\\p{S}${d}]|~)`,J=P(h,D,p),K=P(F,O,z),M=B(h,D,p),S=I(G,N,U);function A(e,t,n){let f=y.test(e);if(!f&&e){const u=t.length-n.length;if(u>=1){let c=u-1,a=t.charCodeAt(c);if(a>=65024&&a<=65038&&c>=1&&(c--,a=t.charCodeAt(c)),(a&64512)===56320&&c>=1){const s=t.codePointAt(c-1);s!==void 0&&s>65535&&(s>=917760&&s<=917999?f=!0:f=k.test(String.fromCodePoint(s)))}else f=k.test(String.fromCharCode(a))}}return f}function R(e,t){if(!(e[1]||e[2]||e[3]||e[4]))return!1;const n=String.fromCodePoint(e[0].codePointAt(0)),f=e.index+e[0].length,u=f<t.length?String.fromCodePoint(t.codePointAt(f)):"";return k.test(n)||k.test(u)}function x(e){return e.length>=5}function W(e){return!!(e[1]||e[2]||e[3]||e[4])}function q(e){return x(e)?!!e[4]:!!e[3]}function v(e){return x(e)?e[1]||e[3]||"":e[1]||e[2]||""}function H(){return{tokenizer:{emStrong(e,t,n=""){const{rules:f}=this,u=f.inline.emStrongLDelim.exec(e);if(!u)return!1;const c=x(u);if(c&&!W(u))return;if(q(u)&&n.match(f.other.unicodeAlphaNumeric))return!1;const a=v(u),s=A(n,t,e);if(!a||!n||f.inline.punctuation.exec(n)||s||k.test(a)){const $=[...u[0]].length-1;let r,o,g=$,i=0;const b=c&&n===u[0][0],C=f.inline.emStrongRDelimAst.source.includes("(?!~)");let m;u[0][0]==="*"?m=C?K:J:m=M,m.lastIndex=0;const _=t.slice(-1*e.length+$);let l;for(;(l=m.exec(_))!=null;){if(r=l[1]||l[2]||l[3]||l[4]||l[5]||l[6],!r)continue;o=[...r].length;const L=R(l,_),Q=!!(l[3]||l[4]),E=!!(l[5]||l[6]);if(Q&&!L){g+=o;continue}if(E||L){if($%3&&!(($+o)%3)){i+=o;continue}if(b&&E)break}if(g-=o,g>0)continue;o=Math.min(o,o+g+i);const V=l[0].codePointAt(0)>65535?2:1,j=e.slice(0,$+l.index+V+o);if(Math.min($,o)%2){const T=j.slice(1,-1);return{type:"em",raw:j,text:T,tokens:this.lexer.inlineTokens(T)}}const w=j.slice(2,-2);return{type:"strong",raw:j,text:w,tokens:this.lexer.inlineTokens(w)}}}},del(e,t,n=""){const{rules:f}=this,u=f.inline.delLDelim.exec(e);if(!u)return!1;const c=u[1]||"",a=A(n,t,e);if(!c||!n||f.inline.punctuation.exec(n)||a||k.test(c)){const s=[...u[0]].length-1;let $,r,o=s;S.lastIndex=0;const g=t.slice(-1*e.length+s);let i;for(;(i=S.exec(g))!=null;){if($=i[1]||i[2]||i[3]||i[4]||i[5]||i[6],!$||(r=[...$].length,r!==s))continue;const b=R(i,g);if((i[3]||i[4])&&!b){o+=r;continue}if(o-=r,o>0)continue;r=Math.min(r,r+o);const C=i[0].codePointAt(0)>65535?2:1,m=e.slice(0,s+i.index+C+r),_=m.slice(s,-s);return{type:"del",raw:m,text:_,tokens:this.lexer.inlineTokens(_)}}}}}}}globalThis.markedCjkFriendly=H})();
|
||||
File diff suppressed because one or more lines are too long
66
packages/coding-agent/test/export-html-cjk.test.ts
Normal file
66
packages/coding-agent/test/export-html-cjk.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { createContext, runInContext } from "node:vm";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { exportFromFile } from "../src/core/export-html/index.ts";
|
||||
import { CURRENT_SESSION_VERSION, type SessionHeader } from "../src/core/session-manager.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
|
||||
function loadMarkdownParser(html: string): (text: string) => string {
|
||||
const sessionData = html.match(/<script id="session-data" type="application\/json">([^<]+)<\/script>/)?.[1];
|
||||
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((match) => match[1]);
|
||||
const viewer = scripts.pop();
|
||||
if (!sessionData || !viewer) throw new Error("Missing exported session data or viewer script");
|
||||
const context = createContext({
|
||||
atob,
|
||||
TextDecoder,
|
||||
URLSearchParams,
|
||||
document: { getElementById: () => ({ textContent: sessionData }), querySelector: () => null },
|
||||
window: { location: { search: "" } },
|
||||
});
|
||||
for (const script of scripts) {
|
||||
runInContext(script, context);
|
||||
}
|
||||
return runInContext(
|
||||
viewer.replace("function safeMarkedParse(", "return safeMarkedParse; function safeMarkedParse("),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
// #10154
|
||||
describe("HTML export CJK emphasis", () => {
|
||||
let tempDir: string;
|
||||
let parse: (text: string) => string;
|
||||
|
||||
beforeAll(async () => {
|
||||
initTheme("dark");
|
||||
tempDir = mkdtempSync(join(tmpdir(), "pi-export-cjk-"));
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: "cjk-test",
|
||||
timestamp: new Date(0).toISOString(),
|
||||
cwd: tempDir,
|
||||
} satisfies SessionHeader;
|
||||
const sessionPath = join(tempDir, "session.jsonl");
|
||||
writeFileSync(sessionPath, `${JSON.stringify(header)}\n`);
|
||||
parse = loadMarkdownParser(readFileSync(await exportFromFile(sessionPath, join(tempDir, "out.html")), "utf8"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("renders emphasis delimited next to CJK punctuation", () => {
|
||||
expect(parse("**这是测试。**后面")).toBe("<p><strong>这是测试。</strong>后面</p>\n");
|
||||
expect(parse("**一句话说清:**一台机器")).toBe("<p><strong>一句话说清:</strong>一台机器</p>\n");
|
||||
expect(parse("**要点到此为止。**20 小时")).toBe("<p><strong>要点到此为止。</strong>20 小时</p>\n");
|
||||
expect(parse("字**「引用」**字")).toBe("<p>字<strong>「引用」</strong>字</p>\n");
|
||||
expect(parse("*斜体。*后面")).toBe("<p><em>斜体。</em>后面</p>\n");
|
||||
});
|
||||
|
||||
it("keeps CommonMark flanking rules for non-CJK text", () => {
|
||||
expect(parse("**end.**Then next")).toBe("<p>**end.**Then next</p>\n");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue