diff --git a/.changeset/quiet-berries-sniff.md b/.changeset/quiet-berries-sniff.md new file mode 100644 index 0000000..d482fa5 --- /dev/null +++ b/.changeset/quiet-berries-sniff.md @@ -0,0 +1,10 @@ +--- +'uniorg-parse': major +'uniorg-stringify': minor +'uniorg-rehype': minor +'uniorg': minor +--- + +Support `line-break` in uniorg, uniorg-parse, uniorg-rehype, and uniorg-stringify. + +This is a breaking change for uniorg-parse as it may output nodes unknown to downstream users (uniorg-rehype and uniorg-stringify). If you upgrade uniorg-parse, you should also upgrade uniorg-rehype and uniorg-stringify to the corresponding versions. diff --git a/README.md b/README.md index 78bacd5..e1fd341 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,6 @@ However, there are a couple of places I haven't finished yet: - babel-call, inline-babel-call, inline-src-block - dynamic-block - target, radio-target -- line-break -- export-snippet - macro - switches and parameters in src-block and example-block - repeater/warning props in timestamp diff --git a/packages/uniorg-parse/src/__snapshots__/parser.spec.ts.snap b/packages/uniorg-parse/src/__snapshots__/parser.spec.ts.snap index 0021453..e81d787 100644 --- a/packages/uniorg-parse/src/__snapshots__/parser.spec.ts.snap +++ b/packages/uniorg-parse/src/__snapshots__/parser.spec.ts.snap @@ -2438,6 +2438,79 @@ children: value: "." `; +exports[`org/parser line-break fake line break (must be at end of line) 1`] = ` +type: "org-data" +contentsBegin: 0 +contentsEnd: 27 +children: + - type: "paragraph" + affiliated: {} + contentsBegin: 0 + contentsEnd: 27 + children: + - type: "text" + value: "some text\\\\" + - type: "entity" + useBrackets: false + name: "not" + latex: "\\\\textlnot{}" + requireLatexMath: false + html: "¬" + ascii: "[angled dash]" + latin1: "¬" + utf8: "¬" + - type: "text" + value: " a line break" +`; + +exports[`org/parser line-break fake line break (triple backslash) 1`] = ` +type: "org-data" +contentsBegin: 0 +contentsEnd: 26 +children: + - type: "paragraph" + affiliated: {} + contentsBegin: 0 + contentsEnd: 26 + children: + - type: "text" + value: "some text\\\\\\\\\\\\\\nnot next line" +`; + +exports[`org/parser line-break valid line break 1`] = ` +type: "org-data" +contentsBegin: 0 +contentsEnd: 22 +children: + - type: "paragraph" + affiliated: {} + contentsBegin: 0 + contentsEnd: 22 + children: + - type: "text" + value: "some text" + - type: "line-break" + - type: "text" + value: "line break" +`; + +exports[`org/parser line-break whitespace after \\\\ 1`] = ` +type: "org-data" +contentsBegin: 0 +contentsEnd: 24 +children: + - type: "paragraph" + affiliated: {} + contentsBegin: 0 + contentsEnd: 24 + children: + - type: "text" + value: "some text" + - type: "line-break" + - type: "text" + value: "line break" +`; + exports[`org/parser links ./ as start of file link 1`] = ` type: "org-data" contentsBegin: 0 diff --git a/packages/uniorg-parse/src/parser.spec.ts b/packages/uniorg-parse/src/parser.spec.ts index 837b8aa..a9f93cc 100644 --- a/packages/uniorg-parse/src/parser.spec.ts +++ b/packages/uniorg-parse/src/parser.spec.ts @@ -1106,4 +1106,25 @@ more text itParses('fake export snippet, missing backend', '@@@@'); }); + + describe('line-break', () => { + itParses( + 'valid line break', + `some text\\\\ +line break` + ); + + itParses('whitespace after \\\\', `some text\\\\ \nline break`); + + itParses( + 'fake line break (must be at end of line)', + `some text\\\\not a line break` + ); + + itParses( + 'fake line break (triple backslash)', + `some text\\\\\\ +not next line` + ); + }); }); diff --git a/packages/uniorg-parse/src/parser.ts b/packages/uniorg-parse/src/parser.ts index b281ced..a62b2df 100644 --- a/packages/uniorg-parse/src/parser.ts +++ b/packages/uniorg-parse/src/parser.ts @@ -53,6 +53,7 @@ import { CitationSuffix, CitationCommonSuffix, ExportSnippet, + LineBreak, } from 'uniorg'; import { getOrgEntity } from './entities.js'; @@ -638,7 +639,9 @@ class Parser { break; case '\\': if (c[1] === '\\') { - // TODO: line break parser + if (restriction.has('line-break')) { + return this.parseLineBreak(); + } } else { const offset = this.r.offset(); const entity = restriction.has('entity') && this.parseEntity(); @@ -1707,6 +1710,19 @@ class Parser { return u('latex-fragment', { value, contents: contents ?? value }); } + private parseLineBreak(): LineBreak | null { + const m = this.r.lookingAt(/\\\\[ \t]*$/m); + if (!m) return null; + + // check character before linebreak + this.r.backoff(1); + if (this.r.peek(1) === '\\') return null; + + this.r.advance(this.r.line()); + + return u('line-break'); + } + private parseFootnoteReference(): FootnoteReference | null { const begin = this.r.offset(); const m = this.r.match(footnoteRe); diff --git a/packages/uniorg-rehype/src/__snapshots__/org-to-hast.spec.ts.snap b/packages/uniorg-rehype/src/__snapshots__/org-to-hast.spec.ts.snap index 72305c0..913a87e 100644 --- a/packages/uniorg-rehype/src/__snapshots__/org-to-hast.spec.ts.snap +++ b/packages/uniorg-rehype/src/__snapshots__/org-to-hast.spec.ts.snap @@ -354,6 +354,12 @@ exports[`org/org-to-hast latex-fragment 1`] = ` `; +exports[`org/org-to-hast line-break 1`] = ` + +

hello
world

+ +`; + exports[`org/org-to-hast link 1`] = `

https://example.com

diff --git a/packages/uniorg-rehype/src/org-to-hast.spec.ts b/packages/uniorg-rehype/src/org-to-hast.spec.ts index 5fee29b..dd49e9a 100644 --- a/packages/uniorg-rehype/src/org-to-hast.spec.ts +++ b/packages/uniorg-rehype/src/org-to-hast.spec.ts @@ -474,6 +474,12 @@ either $$ a=+\\sqrt{2} $$ or \\[ a=-\\sqrt{2} \\].` `[cite/style:common prefix; prefix @key suffix; @key2; common suffix]` ); + hastTest( + 'line-break', + `hello\\\\ +world` + ); + test('respects hProperties', () => { const s = unified() .use(orgParse) diff --git a/packages/uniorg-rehype/src/org-to-hast.ts b/packages/uniorg-rehype/src/org-to-hast.ts index 765f5ed..31d7abe 100644 --- a/packages/uniorg-rehype/src/org-to-hast.ts +++ b/packages/uniorg-rehype/src/org-to-hast.ts @@ -84,6 +84,9 @@ const defaultHandlers: Handlers = { if (org.backEnd !== 'html') return null; return u('raw', org.value) as any; }, + 'line-break': function (org) { + return this.h(org, 'br'); + }, }; function renderAsChildren( diff --git a/packages/uniorg-stringify/src/__snapshots__/stringify.spec.ts.snap b/packages/uniorg-stringify/src/__snapshots__/stringify.spec.ts.snap index 10d4eef..894de81 100644 --- a/packages/uniorg-stringify/src/__snapshots__/stringify.spec.ts.snap +++ b/packages/uniorg-stringify/src/__snapshots__/stringify.spec.ts.snap @@ -295,6 +295,18 @@ either $$ a=+\\\\sqrt{2} $$ or \\\\[ a=-\\\\sqrt{2} \\\\]. " `; +exports[`stringify line-break 1`] = ` +"hello\\\\\\\\ +world! +" +`; + +exports[`stringify line-break with trailing whitespace 1`] = ` +"hello\\\\\\\\ +world! +" +`; + exports[`stringify links angle link 1`] = ` " " diff --git a/packages/uniorg-stringify/src/stringify.spec.ts b/packages/uniorg-stringify/src/stringify.spec.ts index e971ea7..c44333c 100644 --- a/packages/uniorg-stringify/src/stringify.spec.ts +++ b/packages/uniorg-stringify/src/stringify.spec.ts @@ -514,4 +514,11 @@ some text }); test('handle export-snippet', `@@backend:custom value@@`); + + test( + 'line-break', + `hello\\\\ +world!` + ); + test('line-break with trailing whitespace', `hello\\\\ \nworld!`); }); diff --git a/packages/uniorg-stringify/src/stringify.ts b/packages/uniorg-stringify/src/stringify.ts index 93a1b31..03fa0dc 100644 --- a/packages/uniorg-stringify/src/stringify.ts +++ b/packages/uniorg-stringify/src/stringify.ts @@ -18,6 +18,7 @@ export type StringifyOptions = { const defaultOptions: StringifyOptions = { handlers: { 'export-snippet': (org) => `@@${org.backEnd}:${org.value}@@`, + 'line-break': () => `\\\\\n`, }, }; diff --git a/packages/uniorg/src/index.ts b/packages/uniorg/src/index.ts index 665bc12..ebee4a9 100644 --- a/packages/uniorg/src/index.ts +++ b/packages/uniorg/src/index.ts @@ -77,6 +77,7 @@ export type ObjectType = | LatexFragment | Entity | ExportSnippet + | LineBreak | TableCell; export type OrgNode = GreaterElementType | ElementType | ObjectType; @@ -197,6 +198,10 @@ export interface ExportSnippet extends Node { value: string; } +export interface LineBreak extends Node { + type: 'line-break'; +} + export interface List extends GreaterElement { type: 'plain-list'; listType: 'ordered' | 'unordered' | 'descriptive';