Compare commits

..

36 Commits

Author SHA1 Message Date
Tobias Schmitz
ff1d4ded4a
WIP [no ci] 2025-07-01 17:47:55 +02:00
Tobias Schmitz
5543400ff1
fix: [no ci] WIP repeatable table headers/footers 2025-07-01 17:47:55 +02:00
Tobias Schmitz
22bbabca38
fix: [no ci] bug due to table cell start tags in grid layout code 2025-07-01 17:47:55 +02:00
Tobias Schmitz
f948bdcc3f
feat: [no ci] tag table headers and footers 2025-07-01 14:26:43 +02:00
Tobias Schmitz
bf86c28682
feat: [no ci] support headings with level >= 7 2025-07-01 14:26:43 +02:00
Tobias Schmitz
f35d6e5bf9
refactor: [no ci] move link tagging code 2025-07-01 14:26:43 +02:00
Tobias Schmitz
a6b35ec20d
fix: [no ci] don't include outline title in TOC hierarchy 2025-07-01 14:26:43 +02:00
Tobias Schmitz
bb82def4f1
fix: only use link annotation quadpoints when exporting a PDF/UA-1 document 2025-07-01 14:26:43 +02:00
Tobias Schmitz
e2bfc3ffe7
feat: [no ci] hierarchical outline tags 2025-07-01 14:26:43 +02:00
Tobias Schmitz
c54ca7392c
docs: [no ci] fixup some comments 2025-07-01 14:26:43 +02:00
Tobias Schmitz
7decbb3601
feat: [no ci] mark RepeatElem as artifact 2025-07-01 14:26:43 +02:00
Tobias Schmitz
d803a62163
fix: [no ci] mark table gutter and fill as artifacts 2025-07-01 14:26:43 +02:00
Tobias Schmitz
a484ec57c4
feat: always write alt text in marked content sequence for images 2025-07-01 14:26:43 +02:00
Tobias Schmitz
a3f1d630fc
feat: [no ci] add cli args for PDF/UA-1 standard and to disable tagging 2025-07-01 14:26:43 +02:00
Tobias Schmitz
af7c260630
refactor: revert some changes to FrameItem::Link 2025-07-01 14:26:43 +02:00
Tobias Schmitz
11baf1bcca
refactor: [no ci] derive(Cast) for ArtifactKind 2025-07-01 14:26:43 +02:00
Tobias Schmitz
f191b4dd37
fix: [no ci] avoid empty marked-content sequences 2025-07-01 14:26:43 +02:00
Tobias Schmitz
6c8b75efe1
feat: [no ci] generate tags for tables 2025-07-01 14:26:43 +02:00
Tobias Schmitz
4a1fcffd93
feat: [no ci] use local krilla version 2025-07-01 14:26:43 +02:00
Tobias Schmitz
3024d5bbc0
feat: [no ci] pdf.tag function to manually create pdf tags 2025-07-01 14:26:43 +02:00
Tobias Schmitz
0881a3fac5
feat: [no ci] write tags for more elements 2025-07-01 14:26:43 +02:00
Tobias Schmitz
6900854713
feat: [no ci] write tags for links and use quadpoints in link annotations 2025-07-01 14:26:43 +02:00
Tobias Schmitz
ba714c8dbd
feat: pdf.artifact element 2025-07-01 14:26:43 +02:00
Tobias Schmitz
9d38fb4a64
feat: [no ci] mark artifacts 2025-07-01 14:26:43 +02:00
Tobias Schmitz
710356c5af
feat: [WIP] allow specifying alt text for links
skip-checks:true

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Date:      Wed May 28 17:47:35 2025 +0200
#
# On branch pdf-accessibility
# Your branch and 'origin/pdf-accessibility' have diverged,
# and have 11 and 5 different commits each, respectively.
#
# Changes to be committed:
#	modified:   crates/typst-ide/src/jump.rs
#	modified:   crates/typst-layout/src/flow/distribute.rs
#	modified:   crates/typst-layout/src/modifiers.rs
#	modified:   crates/typst-library/src/foundations/content.rs
#	modified:   crates/typst-library/src/layout/frame.rs
#	modified:   crates/typst-library/src/model/bibliography.rs
#	modified:   crates/typst-library/src/model/footnote.rs
#	modified:   crates/typst-library/src/model/link.rs
#	modified:   crates/typst-library/src/model/outline.rs
#	modified:   crates/typst-library/src/model/reference.rs
#	modified:   crates/typst-pdf/src/convert.rs
#	modified:   crates/typst-pdf/src/link.rs
#	modified:   crates/typst-render/src/lib.rs
#	modified:   crates/typst-svg/src/lib.rs
#	modified:   tests/src/run.rs
#
2025-07-01 14:26:43 +02:00
Tobias Schmitz
cc1856ca95
feat: [WIP] include links in tag tree
skip-checks:true
2025-07-01 14:26:43 +02:00
Tobias Schmitz
e8aa619ba5
feat: [WIP] write tags
skip-checks:true
2025-07-01 14:26:43 +02:00
Tobias Schmitz
8356a1a2cb
feat: [WIP] make more things locatable
skip-checks:true
2025-07-01 14:26:43 +02:00
Tobias Schmitz
5a592bbd61
feat: [draft] generate accessibility tag tree for headings
skip-checks:true
2025-07-01 14:26:43 +02:00
Robin
30ddc4a7ca
Fix typos in calc module docs (#6535) 2025-07-01 11:04:31 +00:00
Robin
d978f8c33a
Fix minor typo in array.product docs (#6532) 2025-07-01 11:04:11 +00:00
Adrián Delgado
c99f3ffc7d
Fix typo in PDF standard CLI help part 2 (#6531) 2025-06-30 16:51:36 +00:00
Adrián Delgado
880f56c90d
Fix typo in PDF standard CLI help (#6518) 2025-06-30 08:27:42 +00:00
Robin
a6cf0247b2
Fix typo in Advanced Styling docs tutorial (#6517) 2025-06-30 08:27:22 +00:00
Laurenz
c4bcfb18c1
Support HTML tests in test-helper extension (#6504) 2025-06-30 08:27:02 +00:00
Laurenz
e8f9877fc5
Acknowledgements (#6528) 2025-06-30 08:23:15 +00:00
12 changed files with 289 additions and 102 deletions

View File

@ -240,6 +240,26 @@ instant preview. To achieve these goals, we follow three core design principles:
Luckily we have [`comemo`], a system for incremental compilation which does Luckily we have [`comemo`], a system for incremental compilation which does
most of the hard work in the background. most of the hard work in the background.
## Acknowledgements
We'd like to thank everyone who is supporting Typst's development, be it via
[GitHub sponsors] or elsewhere. In particular, special thanks[^1] go to:
- [Posit](https://posit.co/blog/posit-and-typst/) for financing a full-time
compiler engineer
- [NLnet](https://nlnet.nl/) for supporting work on Typst via multiple grants
through the [NGI Zero Core](https://nlnet.nl/core) fund:
- Work on [HTML export](https://nlnet.nl/project/Typst-HTML/)
- Work on [PDF accessibility](https://nlnet.nl/project/Typst-Accessibility/)
- [Science & Startups](https://www.science-startups.berlin/) for having financed
Typst development from January through June 2023 via the Berlin Startup
Scholarship
- [Zerodha](https://zerodha.tech/blog/1-5-million-pdfs-in-25-minutes/) for their
generous one-time sponsorship
[^1]: This list only includes contributions for our open-source work that exceed
or are expected to exceed €10K.
[docs]: https://typst.app/docs/ [docs]: https://typst.app/docs/
[app]: https://typst.app/ [app]: https://typst.app/
[discord]: https://discord.gg/2uDybryKPe [discord]: https://discord.gg/2uDybryKPe
@ -259,3 +279,4 @@ instant preview. To achieve these goals, we follow three core design principles:
[packages]: https://github.com/typst/packages/ [packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/ [`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst [snap]: https://snapcraft.io/typst
[GitHub sponsors]: https://github.com/sponsors/typst/

View File

@ -498,7 +498,7 @@ pub enum PdfStandard {
/// PDF/A-2u. /// PDF/A-2u.
#[value(name = "a-2u")] #[value(name = "a-2u")]
A_2u, A_2u,
/// PDF/A-3u. /// PDF/A-3b.
#[value(name = "a-3b")] #[value(name = "a-3b")]
A_3b, A_3b,
/// PDF/A-3u. /// PDF/A-3u.

View File

@ -1284,10 +1284,18 @@ impl<'a> GridLayouter<'a> {
if let Some([first, rest @ ..]) = if let Some([first, rest @ ..]) =
frames.get(measurement_data.frames_in_previous_regions..) frames.get(measurement_data.frames_in_previous_regions..)
{ {
// HACK: reconsider if this is the right decision
fn is_empty_frame(frame: &Frame) -> bool {
!frame.items().any(|(_, item)| match item {
FrameItem::Group(group) => is_empty_frame(&group.frame),
FrameItem::Tag(_) => false,
_ => true,
})
}
if can_skip if can_skip
&& breakable && breakable
&& first.is_empty() && is_empty_frame(first)
&& rest.iter().any(|frame| !frame.is_empty()) && rest.iter().any(|frame| !is_empty_frame(frame))
{ {
return Ok(None); return Ok(None);
} }

View File

@ -604,7 +604,7 @@ impl Array {
Ok(acc) Ok(acc)
} }
/// Calculates the product all items (works for all types that can be /// Calculates the product of all items (works for all types that can be
/// multiplied). /// multiplied).
#[func] #[func]
pub fn product( pub fn product(

View File

@ -207,9 +207,9 @@ pub fn sqrt(
/// ``` /// ```
#[func] #[func]
pub fn root( pub fn root(
/// The expression to take the root of /// The expression to take the root of.
radicand: f64, radicand: f64,
/// Which root of the radicand to take /// Which root of the radicand to take.
index: Spanned<i64>, index: Spanned<i64>,
) -> SourceResult<f64> { ) -> SourceResult<f64> {
if index.v == 0 { if index.v == 0 {
@ -317,7 +317,7 @@ pub fn asin(
/// ``` /// ```
#[func(title = "Arccosine")] #[func(title = "Arccosine")]
pub fn acos( pub fn acos(
/// The number whose arcsine to calculate. Must be between -1 and 1. /// The number whose arccosine to calculate. Must be between -1 and 1.
value: Spanned<Num>, value: Spanned<Num>,
) -> SourceResult<Angle> { ) -> SourceResult<Angle> {
let val = value.v.float(); let val = value.v.float();
@ -387,7 +387,7 @@ pub fn cosh(
value.cosh() value.cosh()
} }
/// Calculates the hyperbolic tangent of an hyperbolic angle. /// Calculates the hyperbolic tangent of a hyperbolic angle.
/// ///
/// ```example /// ```example
/// #calc.tanh(0) \ /// #calc.tanh(0) \

View File

@ -817,6 +817,10 @@ pub struct TableCell {
// TODO: feature gate // TODO: feature gate
pub header_scope: Smart<TableHeaderScope>, pub header_scope: Smart<TableHeaderScope>,
// FIXME: this should not be public
#[default(false)]
pub is_repeated: bool,
/// Whether rows spanned by this cell can be placed in different pages. /// Whether rows spanned by this cell can be placed in different pages.
/// When equal to `{auto}`, a cell spanning only fixed-size rows is /// When equal to `{auto}`, a cell spanning only fixed-size rows is
/// unbreakable, while a cell spanning at least one `{auto}`-sized row is /// unbreakable, while a cell spanning at least one `{auto}`-sized row is

View File

@ -179,7 +179,7 @@ pub enum PdfStandard {
/// PDF/A-2u. /// PDF/A-2u.
#[serde(rename = "a-2u")] #[serde(rename = "a-2u")]
A_2u, A_2u,
/// PDF/A-3u. /// PDF/A-3b.
#[serde(rename = "a-3b")] #[serde(rename = "a-3b")]
A_3b, A_3b,
/// PDF/A-3u. /// PDF/A-3u.

View File

@ -230,7 +230,7 @@ impl TableCtx {
} }
a a
}) })
.expect("tables must have at least one column") .unwrap_or(TableCellKind::Data)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -524,7 +524,12 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
push_stack(gc, loc, StackEntryKind::Table(TableCtx::new(table.clone()))); push_stack(gc, loc, StackEntryKind::Table(TableCtx::new(table.clone())));
return; return;
} else if let Some(cell) = elem.to_packed::<TableCell>() { } else if let Some(cell) = elem.to_packed::<TableCell>() {
// TODO: set this in table layout
if cell.is_repeated(StyleChain::default()) {
start_artifact(gc, loc, ArtifactKind::Other);
} else {
push_stack(gc, loc, StackEntryKind::TableCell(cell.clone())); push_stack(gc, loc, StackEntryKind::TableCell(cell.clone()));
}
return; return;
} else if let Some(link) = elem.to_packed::<LinkMarker>() { } else if let Some(link) = elem.to_packed::<LinkMarker>() {
let link_id = gc.tags.next_link_id(); let link_id = gc.tags.next_link_id();

View File

@ -79,7 +79,7 @@ the right.
Last but not least is the `numbering` argument. Here, we can provide a Last but not least is the `numbering` argument. Here, we can provide a
[numbering pattern]($numbering) that defines how to number the pages. By [numbering pattern]($numbering) that defines how to number the pages. By
setting into to `{"1"}`, Typst only displays the bare page number. Setting it to setting it to `{"1"}`, Typst only displays the bare page number. Setting it to
`{"(1/1)"}` would have displayed the current page and total number of pages `{"(1/1)"}` would have displayed the current page and total number of pages
surrounded by parentheses. And we could even have provided a completely custom surrounded by parentheses. And we could even have provided a completely custom
function here to format things to our liking. function here to format things to our liking.

View File

@ -94,9 +94,12 @@
"watch": "tsc -watch -p ./" "watch": "tsc -watch -p ./"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.x", "@types/node": "^24.0.4",
"@types/vscode": "^1.88.0", "@types/vscode": "^1.101.0",
"typescript": "^5.3.3" "typescript": "^5.8.3"
},
"dependencies": {
"shiki": "^3.7.0"
}, },
"engines": { "engines": {
"vscode": "^1.88.0" "vscode": "^1.88.0"

View File

@ -1,6 +1,7 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as cp from "child_process"; import * as cp from "child_process";
import { clearInterval } from "timers"; import { clearInterval } from "timers";
const shiki = import("shiki"); // Normal import causes TypeScript problems.
// Called when an activation event is triggered. Our activation event is the // Called when an activation event is triggered. Our activation event is the
// presence of "tests/suite/playground.typ". // presence of "tests/suite/playground.typ".
@ -17,6 +18,8 @@ class TestHelper {
opened?: { opened?: {
// The tests's name. // The tests's name.
name: string; name: string;
// The test's attributes.
attrs: string[];
// The WebView panel that displays the test images and output. // The WebView panel that displays the test images and output.
panel: vscode.WebviewPanel; panel: vscode.WebviewPanel;
}; };
@ -44,18 +47,18 @@ class TestHelper {
); );
// Triggered when clicking "View" in the lens. // Triggered when clicking "View" in the lens.
this.registerCommand("typst-test-helper.viewFromLens", (name) => this.registerCommand("typst-test-helper.viewFromLens", (name, attrs) =>
this.viewFromLens(name) this.viewFromLens(name, attrs)
); );
// Triggered when clicking "Run" in the lens. // Triggered when clicking "Run" in the lens.
this.registerCommand("typst-test-helper.runFromLens", (name) => this.registerCommand("typst-test-helper.runFromLens", (name, attrs) =>
this.runFromLens(name) this.runFromLens(name, attrs)
); );
// Triggered when clicking "Save" in the lens. // Triggered when clicking "Save" in the lens.
this.registerCommand("typst-test-helper.saveFromLens", (name) => this.registerCommand("typst-test-helper.saveFromLens", (name, attrs) =>
this.saveFromLens(name) this.saveFromLens(name, attrs)
); );
// Triggered when clicking "Terminal" in the lens. // Triggered when clicking "Terminal" in the lens.
@ -121,31 +124,32 @@ class TestHelper {
const lenses = []; const lenses = [];
for (let nr = 0; nr < document.lineCount; nr++) { for (let nr = 0; nr < document.lineCount; nr++) {
const line = document.lineAt(nr); const line = document.lineAt(nr);
const re = /^--- ([\d\w-]+)( [\d\w-]+)* ---$/; const re = /^--- ([\d\w-]+)(( [\d\w-]+)*) ---$/;
const m = line.text.match(re); const m = line.text.match(re);
if (!m) { if (!m) {
continue; continue;
} }
const name = m[1]; const name = m[1];
const attrs = m[2].trim().split(" ");
lenses.push( lenses.push(
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "View", title: "View",
tooltip: "View the test output and reference in a new tab", tooltip: "View the test output and reference in a new tab",
command: "typst-test-helper.viewFromLens", command: "typst-test-helper.viewFromLens",
arguments: [name], arguments: [name, attrs],
}), }),
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "Run", title: "Run",
tooltip: "Run the test and view the results in a new tab", tooltip: "Run the test and view the results in a new tab",
command: "typst-test-helper.runFromLens", command: "typst-test-helper.runFromLens",
arguments: [name], arguments: [name, attrs],
}), }),
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "Save", title: "Save",
tooltip: "Run and view the test and save the reference output", tooltip: "Run and view the test and save the reference output",
command: "typst-test-helper.saveFromLens", command: "typst-test-helper.saveFromLens",
arguments: [name], arguments: [name, attrs],
}), }),
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "Terminal", title: "Terminal",
@ -159,40 +163,49 @@ class TestHelper {
} }
// Triggered when clicking "View" in the lens. // Triggered when clicking "View" in the lens.
private viewFromLens(name: string) { private viewFromLens(name: string, attrs: string[]) {
if (this.opened?.name == name) { if (
this.opened?.name == name &&
this.opened.attrs.join(" ") == attrs.join(" ")
) {
this.opened.panel.reveal(); this.opened.panel.reveal();
return; return;
} }
if (this.opened) { if (this.opened) {
this.opened.name = name; this.opened.name = name;
this.opened.attrs = attrs;
this.opened.panel.title = name; this.opened.panel.title = name;
} else { } else {
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
"typst-test-helper.preview", "typst-test-helper.preview",
name, name,
vscode.ViewColumn.Beside, vscode.ViewColumn.Beside,
{ enableFindWidget: true } { enableFindWidget: true, enableScripts: true }
); );
panel.onDidDispose(() => (this.opened = undefined)); panel.onDidDispose(() => (this.opened = undefined));
panel.webview.onDidReceiveMessage((message) => {
if (message.command === "openFile") {
vscode.env.openExternal(vscode.Uri.parse(message.uri));
}
});
this.opened = { name, panel }; this.opened = { name, attrs, panel };
} }
this.refreshWebView(); this.refreshWebView();
} }
// Triggered when clicking "Run" in the lens. // Triggered when clicking "Run" in the lens.
private runFromLens(name: string) { private runFromLens(name: string, attrs: string[]) {
this.viewFromLens(name); this.viewFromLens(name, attrs);
this.runFromPreview(); this.runFromPreview();
} }
// Triggered when clicking "Run" in the lens. // Triggered when clicking "Run" in the lens.
private saveFromLens(name: string) { private saveFromLens(name: string, attrs: string[]) {
this.viewFromLens(name); this.viewFromLens(name, attrs);
this.saveFromPreview(); this.saveFromPreview();
} }
@ -288,41 +301,37 @@ class TestHelper {
private copyImageFilePathFromPreviewContext(webviewSection: string) { private copyImageFilePathFromPreviewContext(webviewSection: string) {
if (!this.opened) return; if (!this.opened) return;
const { name } = this.opened; const { name } = this.opened;
const { png, ref } = getImageUris(name); const [bucket, format] = webviewSection.split("/");
switch (webviewSection) { vscode.env.clipboard.writeText(
case "png": getUri(name, bucket as Bucket, format as Format).fsPath
vscode.env.clipboard.writeText(png.fsPath); );
break;
case "ref":
vscode.env.clipboard.writeText(ref.fsPath);
break;
default:
break;
}
} }
// Reloads the web view. // Reloads the web view.
private refreshWebView(output?: { stdout: string; stderr: string }) { private refreshWebView(output?: { stdout: string; stderr: string }) {
if (!this.opened) return; if (!this.opened) return;
const { name, panel } = this.opened; const { name, attrs, panel } = this.opened;
const { png, ref } = getImageUris(name);
if (panel) { if (panel) {
console.log( console.log(
`Refreshing WebView for ${name}` + (panel.visible ? " in background" : "")); `Refreshing WebView for ${name}` +
const webViewSrcs = { (panel.visible ? " in background" : "")
png: panel.webview.asWebviewUri(png), );
ref: panel.webview.asWebviewUri(ref),
};
panel.webview.html = ""; panel.webview.html = "";
// Make refresh notable. // Make refresh notable.
setTimeout(() => { setTimeout(async () => {
if (!panel) { if (!panel) {
throw new Error("panel to refresh is falsy after waiting"); throw new Error("panel to refresh is falsy after waiting");
} }
panel.webview.html = getWebviewContent(webViewSrcs, output); panel.webview.html = await getWebviewContent(
panel,
name,
attrs,
output
);
}, 50); }, 50);
} }
} }
@ -386,30 +395,43 @@ function getWorkspaceRoot() {
return vscode.workspace.workspaceFolders![0].uri; return vscode.workspace.workspaceFolders![0].uri;
} }
// Returns the URIs for a test's images. const EXTENSION = { html: "html", render: "png" };
function getImageUris(name: string) {
const root = getWorkspaceRoot(); type Bucket = "store" | "ref";
const png = vscode.Uri.joinPath(root, `tests/store/render/${name}.png`); type Format = "html" | "render";
const ref = vscode.Uri.joinPath(root, `tests/ref/${name}.png`);
return { png, ref }; function getUri(name: string, bucket: Bucket, format: Format) {
let path;
if (bucket === "ref" && format === "render") {
path = `tests/ref/${name}.png`;
} else {
path = `tests/${bucket}/${format}/${name}.${EXTENSION[format]}`;
}
return vscode.Uri.joinPath(getWorkspaceRoot(), path);
} }
// Produces the content of the WebView. // Produces the content of the WebView.
function getWebviewContent( async function getWebviewContent(
webViewSrcs: { png: vscode.Uri; ref: vscode.Uri }, panel: vscode.WebviewPanel,
name: string,
attrs: string[],
output?: { output?: {
stdout: string; stdout: string;
stderr: string; stderr: string;
} }
): string { ): Promise<string> {
const escape = (text: string) => const showHtml = attrs.includes("html");
text.replace(/</g, "&lt;").replace(/>/g, "&gt;"); const showRender = !showHtml || attrs.includes("render");
const stdoutHtml = output?.stdout const stdout = output?.stdout
? `<h1>Standard output</h1><pre>${escape(output.stdout)}</pre>` ? `<h2>Standard output</h2><pre class="output">${escape(
output.stdout
)}</pre>`
: ""; : "";
const stderrHtml = output?.stderr const stderr = output?.stderr
? `<h1>Standard error</h1><pre>${escape(output.stderr)}</pre>` ? `<h2>Standard error</h2><pre class="output">${escape(
output.stderr
)}</pre>`
: ""; : "";
return ` return `
@ -449,46 +471,169 @@ function getWebviewContent(
color: #bebebe; color: #bebebe;
content: "Not present"; content: "Not present";
} }
pre { h2 {
display: inline-block; margin-bottom: 12px;
font-family: var(--vscode-editor-font-family); }
text-align: left; h2 a {
width: 80%; color: var(--vscode-editor-foreground);
text-decoration: underline;
}
h2 a:hover {
cursor: pointer;
} }
.flex { .flex {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
} }
.vertical {
flex-direction: column;
}
.top-bottom {
display: flex;
flex-direction: column;
padding-inline: 32px;
width: calc(100vw - 64px);
}
pre {
font-family: var(--vscode-editor-font-family);
text-align: left;
white-space: pre-wrap;
}
pre.output {
display: inline-block;
width: 80%;
margin-block-start: 0;
}
pre.shiki {
background-color: transparent !important;
padding: 12px;
margin-block-start: 0;
}
pre.shiki code {
--vscode-textPreformat-background: transparent;
}
iframe, pre.shiki {
border: 1px solid rgb(189, 191, 204);
border-radius: 6px;
}
iframe {
background: white;
}
.vscode-dark iframe {
filter: invert(1) hue-rotate(180deg);
}
</style> </style>
<script>
const api = acquireVsCodeApi()
function openFile(uri) {
api.postMessage({ command: 'openFile', uri });
}
function sizeIframe(obj){
obj.style.height = 0;
obj.style.height = obj.contentWindow.document.body.scrollHeight + 'px';
}
</script>
</head> </head>
<body> <body>
<div ${showRender ? renderSection(panel, name) : ""}
${showHtml ? await htmlSection(name) : ""}
${stdout}
${stderr}
</body>
</html>`;
}
function renderSection(panel: vscode.WebviewPanel, name: string) {
const outputUri = getUri(name, "store", "render");
const refUri = getUri(name, "ref", "render");
return `<div
class="flex" class="flex"
data-vscode-context='{"preventDefaultContextMenuItems": true}' data-vscode-context='{"preventDefaultContextMenuItems": true}'
> >
<div> <div>
<h1>Output</h1> ${linkedTitle("Output", outputUri)}
<img <img
class="output" class="output"
data-vscode-context='{"webviewSection":"png"}' data-vscode-context='{"bucket":"store", format: "render"}'
src="${webViewSrcs.png}" src="${panel.webview.asWebviewUri(outputUri)}"
alt="Placeholder" alt="Placeholder"
> >
</div> </div>
<div> <div>
<h1>Reference</h1> ${linkedTitle("Reference", refUri)}
<img <img
class="ref" class="ref"
data-vscode-context='{"webviewSection":"ref"}' data-vscode-context='{"bucket":"ref", format: "render"}'
src="${webViewSrcs.ref}" src="${panel.webview.asWebviewUri(refUri)}"
alt="Placeholder" alt="Placeholder"
> >
</div> </div>
</div> </div>`;
${stdoutHtml} }
${stderrHtml}
</body> async function htmlSection(name: string) {
</html>`; const storeHtml = await htmlSnippet(
"HTML Output",
getUri(name, "store", "html")
);
const refHtml = await htmlSnippet(
"HTML Reference",
getUri(name, "ref", "html")
);
return `<div
class="flex vertical"
data-vscode-context='{"preventDefaultContextMenuItems": true}'
>
${storeHtml}
${refHtml}
</div>`;
}
async function htmlSnippet(title: string, uri: vscode.Uri): Promise<string> {
try {
const data = await vscode.workspace.fs.readFile(uri);
const code = new TextDecoder("utf-8").decode(data);
return `<div>
${linkedTitle(title, uri)}
<div class="top-bottom">
${await highlight(code)}
<iframe srcdoc="${escape(code)}"></iframe>
</div>
</div>`;
} catch {
return `<div><h2>${title}</h2>Not present</div>`;
}
}
function linkedTitle(title: string, uri: vscode.Uri) {
return `<h2><a onclick="openFile('${uri.toString()}')">${title}</a></h2>`;
}
async function highlight(code: string): Promise<string> {
return (await shiki).codeToHtml(code, {
lang: "html",
theme: selectTheme(),
});
}
function selectTheme() {
switch (vscode.window.activeColorTheme.kind) {
case vscode.ColorThemeKind.Light:
case vscode.ColorThemeKind.HighContrastLight:
return "github-light";
case vscode.ColorThemeKind.Dark:
case vscode.ColorThemeKind.HighContrast:
return "github-dark";
}
}
function escape(text: string) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
} }

View File

@ -1,9 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "Node16", "module": "nodenext",
"lib": ["ES2022", "DOM"],
"target": "ES2022", "target": "ES2022",
"moduleResolution": "nodenext",
"outDir": "dist", "outDir": "dist",
"lib": ["ES2022"],
"sourceMap": true, "sourceMap": true,
"rootDir": "src", "rootDir": "src",
"strict": true "strict": true