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__/minimal-book-breakLongAttributes-true.ptx b/packages/format/src/lib/__snapshots__/minimal-book-breakLongAttributes-true.ptx
new file mode 100644
index 0000000..82c6747
--- /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 3f40147..eb561d0 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 f539d78..c0f0fa1 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 6fac9d1..d5ddc94 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
diff --git a/packages/format/src/lib/__snapshots__/runestone.ptx b/packages/format/src/lib/__snapshots__/runestone.ptx
index 00920bd..c7e33bf 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 a887d31..c919bc0 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();
-
+
+
`;
+ 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);
+ });
+
});
diff --git a/packages/format/src/lib/format.ts b/packages/format/src/lib/format.ts
index c2e2964..495ec41 100644
--- a/packages/format/src/lib/format.ts
+++ b/packages/format/src/lib/format.ts
@@ -13,6 +13,8 @@ import {
export interface FormatOptions {
breakLines?: "few" | "some" | "many";
breakSentences?: boolean;
+ /** Wrap long block start-tag attributes onto separate lines. */
+ breakLongAttributes?: boolean;
insertSpaces?: boolean;
tabSize?: number;
/** Target line width for paragraph text reflow. 0 = no width limit. Default 80. */
@@ -23,6 +25,7 @@ interface Ctx {
ind: string; // one indent unit (e.g. " "); caller repeats it per depth level
blankLines: "few" | "some" | "many";
breakSentences: boolean;
+ breakLongAttributes: boolean;
printWidth: number;
}
@@ -31,9 +34,10 @@ function makeCtx(options?: FormatOptions): Ctx {
const tabSize = options?.tabSize ?? 2;
const insertSpaces = options?.insertSpaces ?? true;
const breakSentences = options?.breakSentences ?? false;
+ const breakLongAttributes = options?.breakLongAttributes ?? false;
const printWidth = options?.printWidth ?? 80;
const ind = insertSpaces ? " ".repeat(tabSize) : "\t";
- return { ind, blankLines, breakSentences, printWidth };
+ return { ind, blankLines, breakSentences, breakLongAttributes, printWidth };
}
/**
@@ -191,20 +195,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 +205,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 ─────────────────────────────────────────────────────────────────
@@ -374,7 +373,7 @@ function appendBlock(
): void {
const ind = ctx.ind.repeat(depth);
if (isEmptyElement(node)) {
- out.push(`${ind}${selfClose(node)}`);
+ out.push(...startTagLines(node, depth, ctx, true));
return;
}
@@ -383,15 +382,22 @@ function appendBlock(
const mc = meaningfulChildren(node);
if (mc.length === 1 && mc[0].type === "element" && (mc[0] as Element).name === "xi:include") {
const el = mc[0] as Element;
- const inner = isEmptyElement(el)
- ? selfClose(el)
- : `${openTag(el)}${inlineSerialize(el.children)}${el.name}>`;
- out.push(`${ind}${openTag(node)}${inner}${node.name}>`);
+ const startLines = startTagLines(node, depth, ctx);
+ if (startLines.length === 1) {
+ const inner = isEmptyElement(el)
+ ? selfClose(el)
+ : `${openTag(el)}${inlineSerialize(el.children)}${el.name}>`;
+ out.push(`${startLines[0]}${inner}${node.name}>`);
+ return;
+ }
+ out.push(...startLines);
+ appendNode(el, out, depth + 1, ctx);
+ out.push(`${ind}${node.name}>`);
return;
}
//Add starting tag as its own line.
- out.push(`${ind}${openTag(node)}`);
+ out.push(...startTagLines(node, depth, ctx));
for (const child of meaningfulChildren(node)) {
appendNode(child, out, depth + 1, ctx);
}
@@ -540,11 +546,41 @@ function selfClose(node: Element): string {
}
function buildAttrs(node: Element): string {
+ return buildAttrList(node).join(" ");
+}
+
+function buildAttrList(node: Element): string[] {
return Object.entries(node.attributes || {})
// v == null (loose equality) covers both null and undefined that can appear
// in Object.entries output for boolean/valueless XML attributes.
.map(([k, v]) => (v == null ? k : `${k}="${escAttr(v)}"`))
- .join(" ");
+}
+
+function startTagLines(
+ node: Element,
+ depth: number,
+ ctx: Ctx,
+ selfClosing = false,
+): string[] {
+ const ind = ctx.ind.repeat(depth);
+ const attrs = buildAttrList(node);
+ const close = selfClosing ? "/>" : ">";
+ if (attrs.length === 0) {
+ return [`${ind}<${node.name}${close}`];
+ }
+
+ const singleLine = `${ind}<${node.name} ${attrs.join(" ")}${close}`;
+ if (!ctx.breakLongAttributes || ctx.printWidth === 0 || singleLine.length <= ctx.printWidth) {
+ return [singleLine];
+ }
+
+ const continuationIndent = `${ind}${" ".repeat(node.name.length + 2)}`;
+ const lines = [`${ind}<${node.name} ${attrs[0]}`];
+ for (const attr of attrs.slice(1)) {
+ lines.push(`${continuationIndent}${attr}`);
+ }
+ lines[lines.length - 1] += close;
+ return lines;
}
// ─── Predicates ───────────────────────────────────────────────────────────────
diff --git a/packages/vscode-extension/src/lsp-server/formatter-ptx.ts b/packages/vscode-extension/src/lsp-server/formatter-ptx.ts
index 032fc61..4a9a139 100644
--- a/packages/vscode-extension/src/lsp-server/formatter-ptx.ts
+++ b/packages/vscode-extension/src/lsp-server/formatter-ptx.ts
@@ -13,6 +13,7 @@ function getOptions() {
return {
breakSentences: globalSettings.formatter.breakSentences,
breakLines: globalSettings.formatter.blankLines,
+ breakLongAttributes: globalSettings.formatter.breakLongAttributes,
tabSize: globalSettings.editor.tabSize,
insertSpaces: globalSettings.editor.insertSpaces,
printWidth: globalSettings.formatter.printWidth,
diff --git a/packages/vscode-extension/src/lsp-server/main.ts b/packages/vscode-extension/src/lsp-server/main.ts
index 0acfb52..94564d0 100644
--- a/packages/vscode-extension/src/lsp-server/main.ts
+++ b/packages/vscode-extension/src/lsp-server/main.ts
@@ -158,6 +158,7 @@ interface LspSettings {
formatter: {
breakSentences: boolean;
blankLines: "few" | "some" | "many";
+ breakLongAttributes: boolean;
printWidth: number;
};
editor: {
@@ -174,7 +175,12 @@ const insertSpacesConfigSection = "editor.insertSpaces";
// The global settings, used when the `workspace/configuration` request is not supported by the client.
const defaultSettings: LspSettings = {
schema: { versionName: "Stable", customPath: "" },
- formatter: { blankLines: "some", breakSentences: true, printWidth: 80 },
+ formatter: {
+ blankLines: "some",
+ breakSentences: true,
+ breakLongAttributes: false,
+ printWidth: 80,
+ },
editor: { tabSize: 2, insertSpaces: true },
};
export let globalSettings: LspSettings = defaultSettings;