Inline code like print("hello") should remain inline and not be broken across lines.
+
Inline code with surrounding spaces x + 1 should preserve those spaces.
+
A program with a trailing blank line:
+
+def foo():
+ return 1
+
+
A console session:
@@ -49,8 +56,25 @@ f.factor()
edgePercentage: 0.35
});
anim.doDetectCycle();
-
+
+
+
+ //line A
+//line B
+
+
+
+
+//line D
+
+
+
diff --git a/packages/format/src/lib/__snapshots__/runestone.ptx b/packages/format/src/lib/__snapshots__/runestone.ptx
index 00920bda..c7e33bf8 100644
--- a/packages/format/src/lib/__snapshots__/runestone.ptx
+++ b/packages/format/src/lib/__snapshots__/runestone.ptx
@@ -90,7 +90,9 @@ along with MathBook XML. If not, see .
- print("Hello, World!")
+
+ print("Hello, World!")
+
@@ -107,7 +109,9 @@ along with MathBook XML. If not, see .
- print("Hello, World!")
+
+ print("Hello, World!")
+
@@ -162,7 +166,9 @@ along with MathBook XML. If not, see .
- document.write('Hello, world!');
+
+ document.write('Hello, world!');
+
@@ -296,7 +302,6 @@ along with MathBook XML. If not, see .
else:
print("Test failed")
-
A program can have a preamble and/or postamble which
are added to the code that the user writes before it is run. They are
@@ -333,11 +338,14 @@ along with MathBook XML. If not, see .
- def add(a, b):
+ def add(a, b):
- # TODO - complete the add function
+
+ # TODO - complete the add function
+
- # Use the function
+
+ # Use the function
result = add(2, 3)
if result == 5:
print("Test passed")
@@ -522,7 +530,9 @@ along with MathBook XML. If not, see .
- SELECT * FROM test
+
+ SELECT * FROM test
+
assert 1,1 == world
assert 0,1 == hello
@@ -565,7 +575,9 @@ along with MathBook XML. If not, see .
add.h (version 1) - A very simple header file that lacks header guards.
- int add(int a, int b);
+
+ int add(int a, int b);
+
@@ -624,7 +636,6 @@ along with MathBook XML. If not, see .
cout << "The sum of " << a << " and " << b << " is " << add(a, b) << endl;
}]]>
-
Note that there is a cross page test of add-files located in
@@ -651,7 +662,9 @@ along with MathBook XML. If not, see .
A Python program, stepable with CodeLens
- print('Hello, World!')
+
+ print('Hello, World!')
+
@@ -741,8 +754,7 @@ along with MathBook XML. If not, see .
-
-.
-
-.
-
-
const int len = 20;
int main() {
@@ -860,8 +870,7 @@ along with MathBook XML. If not, see .
-
-
-
-
using namespace std;
int main() {
@@ -1145,7 +1153,9 @@ TEST_CASE( "Test the add function" ) {
1
-
+
3
@@ -1408,8 +1418,7 @@ TEST_CASE( "Test the add function" ) {
-
-
-
a blue square
@@ -7472,7 +7480,6 @@ TEST_CASE( "Test the add function" ) {
-
s (feet)
@@ -7498,11 +7505,9 @@ TEST_CASE( "Test the add function" ) {
-
Find the average velocity of the car on the interval 0 \le t \le .
-
($$s(3*$a)-$$s(0*$a))/(3$a)
@@ -7514,7 +7519,6 @@ TEST_CASE( "Test the add function" ) {
Find the average velocity of the car on the interval [, ].
-
($$s(($c+2)*$a)-$$s($c*$a))/(2$a)
@@ -7802,4 +7806,4 @@ TEST_CASE( "Test the add function" ) {
-
+
\ No newline at end of file
diff --git a/packages/format/src/lib/__snapshots__/verbatim-blocks.ptx b/packages/format/src/lib/__snapshots__/verbatim-blocks.ptx
index a887d318..c919bc0b 100644
--- a/packages/format/src/lib/__snapshots__/verbatim-blocks.ptx
+++ b/packages/format/src/lib/__snapshots__/verbatim-blocks.ptx
@@ -28,7 +28,9 @@ x = var('x')
f = x^2 + 3*x + 2
f.factor()
-
+
@@ -36,12 +38,26 @@ f.factor()
broken across lines.
+
+ Inline code with surrounding spaces x + 1 should preserve those
+ spaces.
+
Fast forward to the end of the animation and then click New Graph
button to generate a new random graph.
@@ -75,7 +90,23 @@ f.factor()
});
anim.doDetectCycle();
-
+
+
+
+ //line A
+//line B
+
+
+
+
+//line D
+
+
\ No newline at end of file
diff --git a/packages/format/src/lib/docStructure.ts b/packages/format/src/lib/docStructure.ts
index 70d20ced..9b08da1b 100644
--- a/packages/format/src/lib/docStructure.ts
+++ b/packages/format/src/lib/docStructure.ts
@@ -178,6 +178,8 @@ export const verbatimTags = [
"macros",
"prefigure",
"program",
+ "preamble",
+ "postamble",
"input",
"output",
"prompt",
diff --git a/packages/format/src/lib/format.spec.ts b/packages/format/src/lib/format.spec.ts
index 20ed7b34..90e62111 100644
--- a/packages/format/src/lib/format.spec.ts
+++ b/packages/format/src/lib/format.spec.ts
@@ -1,3 +1,4 @@
+import { skip } from "node:test";
import { formatPretext } from "./format";
describe("format", () => {
@@ -24,3 +25,50 @@ describe("format", () => {
expect(result).not.toMatch(/]*\/>\s*\n\s*<\/webwork>/);
});
});
+
+describe("verbatim content preservation", () => {
+ it("preserves trailing space in single-line verbatim (e.g. $ )", () => {
+ const input = `$ ls`;
+ const result = formatPretext(input);
+ expect(result).toContain("$ ");
+ });
+
+ it("preserves leading and trailing spaces in inline tags", () => {
+ const input = `
Inline x + 1 with spaces.
`;
+ const result = formatPretext(input);
+ expect(result).toContain(" x + 1 ");
+ });
+
+ it("preserves intentional trailing blank line inside a program block", () => {
+ const input = `\ndef foo():\n return 1\n\n`;
+ const result = formatPretext(input);
+ // The blank line at the end of the code should survive formatting
+ expect(result).toMatch(/return 1\n\n/);
+ });
+
+ it("does not alter internal whitespace or indentation in code blocks", () => {
+ const code = " line1\n line2 indented\n line3";
+ const input = `
\n${code}\n
`;
+ const result = formatPretext(input);
+ expect(result).toContain(code);
+ });
+
+ it("preserves internal blank lines inside verbatim blocks", () => {
+ const input = `\nline1\n\nline3\n`;
+ const result = formatPretext(input);
+ expect(result).toMatch(/line1\n\nline3/);
+ });
+
+ it("preserves boundary newlines in verbatim blocks", () => {
+ const input = ``;
+ const result = formatPretext(input);
+ expect(result).toBe(input);
+ });
+
+
+ //it("preserves indentation adjacent to a verbatim closing tag", () => {
+ // const input = `\n print("hi")\n `;
+ // const result = formatPretext(input);
+ // expect(result).toBe(input);
+ //});
+});
diff --git a/packages/format/src/lib/format.ts b/packages/format/src/lib/format.ts
index c2e29647..d9f2d2fb 100644
--- a/packages/format/src/lib/format.ts
+++ b/packages/format/src/lib/format.ts
@@ -191,20 +191,9 @@ function appendVerbatim(
out.push(`${ind}${selfClose(node)}`);
return;
}
- const raw = extractVerbatimContent(node);
- // Single-line verbatim (e.g. print(x)) stays on one line. Applies to sage code as well.
- // We choose to remove whitespace padding around the content in this case as well, since it's more likely to be accidental and visually distracting than intentional when the content is short enough to fit on one line.
- if (!raw.includes("\n")) {
- out.push(`${ind}${openTag(node)}${raw.trim()}${node.name}>`);
- return;
- }
- out.push(`${ind}${openTag(node)}`);
- // Lines are pushed without re-indenting so code/math content is preserved exactly.
- for (const line of raw.split("\n")) out.push(line);
- out.push(`${ind}${node.name}>`);
-}
-function extractVerbatimContent(node: Element): string {
+ // Preserve verbatim inner content exactly as parsed (including newlines and
+ // trailing spaces), while still escaping text-node XML entities.
const raw = node.children
.map((c) => {
if (c.type === "text") return escText(c.value);
@@ -212,10 +201,16 @@ function extractVerbatimContent(node: Element): string {
return "";
})
.join("");
- // Authors typically write \ncode\n; strip the surrounding newlines
- // so the content lines themselves set the indentation, not the tag placement.
- // /(\n\s*)*$/ removes all trailing blank/whitespace-only lines, not just one \n.
- return raw.replace(/^\n/, "").replace(/(\n\s*)*$/, "");
+ const trailingNewlineWithWhitespace = /\n[ \t]*$/.test(raw);
+ if (trailingNewlineWithWhitespace) {
+ // Strip any trailing whitespace after the final newline so the closing tag
+ // gets the correct indentation.
+ const trimmedRaw = raw.replace(/\n[ \t]*$/, "\n");
+ out.push(`${ind}${openTag(node)}${trimmedRaw}${ind}${node.name}>`);
+ } else {
+ // Otherwise, render the whole verbatim element on one line. Any internal newlines will be preserved as literal \n characters in the text content, and any trailing spaces will be preserved because the closing tag is on the same line.
+ out.push(`${ind}${openTag(node)}${raw}${node.name}>`);
+ }
}
// ─── Line-end ─────────────────────────────────────────────────────────────────
From eb07adf04cdbabe2f91345da0168d711bd03840c Mon Sep 17 00:00:00 2001
From: Oscar Levin
Date: Fri, 29 May 2026 21:58:05 -0600
Subject: [PATCH 2/3] add option to align too-long attributes on their own
lines
---
extension/package.json | 6 ++
packages/format/README.md | 2 +
packages/format/cli.cjs | 7 ++-
.../src/lib/__fixtures__/minimal-book.ptx | 2 +-
.../minimal-book-breakLongAttributes-true.ptx | 58 +++++++++++++++++++
.../lib/__snapshots__/minimal-book-few.ptx | 2 +-
.../lib/__snapshots__/minimal-book-many.ptx | 2 +-
.../lib/__snapshots__/minimal-book-tabs.ptx | 2 +-
.../src/lib/__snapshots__/minimal-book.ptx | 2 +-
.../format/src/lib/format-snapshots.spec.ts | 11 ++++
packages/format/src/lib/format.spec.ts | 14 ++++-
packages/format/src/lib/format.ts | 57 +++++++++++++++---
.../src/lsp-server/formatter-ptx.ts | 1 +
.../vscode-extension/src/lsp-server/main.ts | 8 ++-
14 files changed, 158 insertions(+), 16 deletions(-)
create mode 100644 packages/format/src/lib/__snapshots__/minimal-book-breakLongAttributes-true.ptx
diff --git a/extension/package.json b/extension/package.json
index 46b35568..df411289 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -290,6 +290,12 @@
"default": true,
"markdownDescription": "Whether to add a line break after each period in a paragraph."
},
+ "pretext-tools.formatter.breakLongAttributes": {
+ "order": 4,
+ "type": "boolean",
+ "default": false,
+ "markdownDescription": "Wrap long block start-tag attributes onto their own lines."
+ },
"pretext-tools.formatter.printWidth": {
"order": 5,
"type": "number",
diff --git a/packages/format/README.md b/packages/format/README.md
index f5ac2d03..d334952f 100644
--- a/packages/format/README.md
+++ b/packages/format/README.md
@@ -22,6 +22,7 @@ You can pass options to customize formatting:
const options = {
breakLines: "many",
breakSentences: true,
+ breakLongAttributes: true,
printWidth: 80,
insertSpaces: true,
tabSize: 2,
@@ -79,6 +80,7 @@ Options:
- `--stdin` read input from stdin
- `--break-lines ` choose line break density
- `--break-sentences` break plain-text sentences onto new lines
+- `--break-long-attributes` wrap long block start-tag attributes onto their own lines
- `--tab-size ` set spaces per indent level
- `--use-tabs` indent with tabs instead of spaces
- `-h, --help` show help
diff --git a/packages/format/cli.cjs b/packages/format/cli.cjs
index b52a583b..7b42f4e8 100644
--- a/packages/format/cli.cjs
+++ b/packages/format/cli.cjs
@@ -15,6 +15,7 @@ Options:
--stdin Read input from stdin
--break-lines Line break mode: few | some | many
--break-sentences Break plain-text sentences onto new lines
+ --break-long-attributes Wrap long block start-tag attributes onto their own lines
--tab-size Number of spaces per indent level
--use-tabs Indent with tabs instead of spaces
-h, --help Show this help
@@ -36,6 +37,7 @@ function parseCli() {
stdin: { type: "boolean", default: false },
"break-lines": { type: "string" },
"break-sentences": { type: "boolean", default: false },
+ "break-long-attributes": { type: "boolean", default: false },
"tab-size": { type: "string" },
"use-tabs": { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
@@ -50,7 +52,7 @@ function parseCli() {
}
function parseOptions(values) {
- /** @type {{breakLines?: "few" | "some" | "many"; breakSentences?: boolean; insertSpaces?: boolean; tabSize?: number}} */
+ /** @type {{breakLines?: "few" | "some" | "many"; breakSentences?: boolean; breakLongAttributes?: boolean; insertSpaces?: boolean; tabSize?: number}} */
const formatOptions = {};
if (values["break-lines"] !== undefined) {
if (!["few", "some", "many"].includes(values["break-lines"])) {
@@ -61,6 +63,9 @@ function parseOptions(values) {
if (values["break-sentences"]) {
formatOptions.breakSentences = true;
}
+ if (values["break-long-attributes"]) {
+ formatOptions.breakLongAttributes = true;
+ }
if (values["use-tabs"]) {
formatOptions.insertSpaces = false;
}
diff --git a/packages/format/src/lib/__fixtures__/minimal-book.ptx b/packages/format/src/lib/__fixtures__/minimal-book.ptx
index 6c6967ac..3c95d067 100644
--- a/packages/format/src/lib/__fixtures__/minimal-book.ptx
+++ b/packages/format/src/lib/__fixtures__/minimal-book.ptx
@@ -1,3 +1,3 @@
My
BookJane
- DoeSome University2024
This is a very short abstract for the book.
Introduction
This is the first paragraph of the introduction chapter. It has some text that is reasonably long.
This is a second paragraph.
First Section
Some content here in the first section.
Second Section
Content in the second section.
+ DoeSome University2024
This is a very short abstract for the book.
Introduction
This is the first paragraph of the introduction chapter. It has some text that is reasonably long.
This is a second paragraph.
First Section
Some content here in the first section.
Second Section
Content in the second section.
diff --git a/packages/format/src/lib/__snapshots__/minimal-book-breakLongAttributes-true.ptx b/packages/format/src/lib/__snapshots__/minimal-book-breakLongAttributes-true.ptx
new file mode 100644
index 00000000..82c6747f
--- /dev/null
+++ b/packages/format/src/lib/__snapshots__/minimal-book-breakLongAttributes-true.ptx
@@ -0,0 +1,58 @@
+
+
+
+
+ My Book
+
+
+
+
+ Jane Doe
+ Some University
+
+ 2024
+
+
+
+
+ This is a very short abstract for the book.
+
+
+
+
+
+ Introduction
+
+
+ This is the first paragraph of the introduction chapter. It has some
+ text that is reasonably long.
+
+
+
+ This is a second paragraph.
+
+
+
+ First Section
+
+
+ Some content here in the first section.
+
+
+
+
+ Second Section
+
+
+ Content in the second section.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/format/src/lib/__snapshots__/minimal-book-few.ptx b/packages/format/src/lib/__snapshots__/minimal-book-few.ptx
index 3f401474..eb561d08 100644
--- a/packages/format/src/lib/__snapshots__/minimal-book-few.ptx
+++ b/packages/format/src/lib/__snapshots__/minimal-book-few.ptx
@@ -17,7 +17,7 @@
-
+ Introduction
This is the first paragraph of the introduction chapter. It has some
diff --git a/packages/format/src/lib/__snapshots__/minimal-book-many.ptx b/packages/format/src/lib/__snapshots__/minimal-book-many.ptx
index f539d789..c0f0fa13 100644
--- a/packages/format/src/lib/__snapshots__/minimal-book-many.ptx
+++ b/packages/format/src/lib/__snapshots__/minimal-book-many.ptx
@@ -32,7 +32,7 @@
-
+ Introduction
diff --git a/packages/format/src/lib/__snapshots__/minimal-book-tabs.ptx b/packages/format/src/lib/__snapshots__/minimal-book-tabs.ptx
index 6fac9d10..d5ddc941 100644
--- a/packages/format/src/lib/__snapshots__/minimal-book-tabs.ptx
+++ b/packages/format/src/lib/__snapshots__/minimal-book-tabs.ptx
@@ -20,7 +20,7 @@
-
+ Introduction