@osdlc/frontmatter-lossless 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +15 -0
- package/dist/index.d.ts +46 -4
- package/dist/index.js +37 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2f3fc7c: Add `newShape(opts?)`, with its options type `NewShapeOptions`: the
|
|
8
|
+
`NoteShape` for a new note, one with no source text to `split`. Both fences
|
|
9
|
+
end in `eol` (`'\n'` by default, or `'\r\n'`), and `bom` (default `false`)
|
|
10
|
+
adds a byte-order mark. `compose(yaml, body, newShape())` gives
|
|
11
|
+
`---\n<yaml>\n---\n<body>`. Pass the YAML text the way `split` returns it,
|
|
12
|
+
without the line break before the closing fence. Until now there was no
|
|
13
|
+
supported way to write a new note, because a `NoteShape` must not be built by
|
|
14
|
+
hand.
|
|
15
|
+
- 2f3fc7c: `Split` has a new `unterminated` field. It is `true` when a note starts with a
|
|
16
|
+
valid opening fence (`---` and a line break, after an optional BOM) and no
|
|
17
|
+
closing `---` line follows. `yaml` is still `null` and `body` the whole text,
|
|
18
|
+
as before, so only a caller that wants to report the note as malformed needs
|
|
19
|
+
to read it. Until now such a caller had to repeat the opening-fence check
|
|
20
|
+
itself.
|
|
21
|
+
|
|
3
22
|
## 1.0.0
|
|
4
23
|
|
|
5
24
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ compose(yaml, body, shape) === note // true
|
|
|
60
60
|
found only when the note starts with `---` and a line break, after an
|
|
61
61
|
optional BOM, and is closed by `---` alone on a line. Anything else,
|
|
62
62
|
including a block with no closing fence, is body. It never throws.
|
|
63
|
+
`unterminated` is `true` when the note opens a block that never closes, so
|
|
64
|
+
a caller can report that note as malformed rather than read it as body.
|
|
63
65
|
- `compose(frontmatter, body, shape)` rebuilds the note. `frontmatter` is
|
|
64
66
|
`null` for no block, `''` for an empty block, or the YAML text. Passing back
|
|
65
67
|
what `split` returned gives the original text.
|
|
@@ -67,11 +69,24 @@ compose(yaml, body, shape) === note // true
|
|
|
67
69
|
`'\r\n'`), `closeEol` (a `CloseEol`, which can also be `''` when the note
|
|
68
70
|
ends at the closing fence) and `closeSep`. Take it from `split` and pass it
|
|
69
71
|
to `compose`. Do not build one by hand: a minor release may add a field.
|
|
72
|
+
- `newShape(opts?)` returns the `NoteShape` for a new note that has no source
|
|
73
|
+
text to split. Its `NewShapeOptions` are `eol` (`'\n'` by default, or
|
|
74
|
+
`'\r\n'`) for both fences and `bom` (`false` by default). Pass the YAML
|
|
75
|
+
text the way `split` returns it, without the line break before the closing
|
|
76
|
+
fence: trim the trailing newline a YAML serializer adds.
|
|
70
77
|
- `splitLenient(content)` reads hand-written input that `split` rejects: blank lines or indentation
|
|
71
78
|
before the opening fence, and trailing spaces on the fence lines. The closing fence must not be
|
|
72
79
|
indented. It returns a `LenientSplit` (`yaml` and `body`), or `null` when there is no block. It
|
|
73
80
|
keeps no framing, so use it to read, not to rewrite.
|
|
74
81
|
|
|
82
|
+
```ts
|
|
83
|
+
import { compose, newShape } from '@osdlc/frontmatter-lossless'
|
|
84
|
+
|
|
85
|
+
const yaml = 'title: New\ntags: [a]\n' // as a YAML serializer writes it
|
|
86
|
+
compose(yaml.replace(/\n$/, ''), '# New\n', newShape())
|
|
87
|
+
// '---\ntitle: New\ntags: [a]\n---\n# New\n'
|
|
88
|
+
```
|
|
89
|
+
|
|
75
90
|
Offsets are JavaScript string indices (UTF-16 code units):
|
|
76
91
|
`src.slice(yamlRange.start, yamlRange.end) === yaml` and
|
|
77
92
|
`src.slice(bodyStart) === body`.
|
package/dist/index.d.ts
CHANGED
|
@@ -46,9 +46,10 @@ export type CloseEol = '' | '\n' | '\r\n';
|
|
|
46
46
|
* file whose fences were `---\r\n`, and now every line of the note shows as
|
|
47
47
|
* changed in git. The fences are data too.
|
|
48
48
|
*
|
|
49
|
-
* Take it from {@link split} and pass it back to {@link compose}
|
|
50
|
-
*
|
|
51
|
-
*
|
|
49
|
+
* Take it from {@link split} and pass it back to {@link compose}, or, for a
|
|
50
|
+
* new note with no source text, from {@link newShape}. Do not build one by
|
|
51
|
+
* hand: a minor release may add a field, which would break an object literal
|
|
52
|
+
* typed as `NoteShape`.
|
|
52
53
|
*/
|
|
53
54
|
export interface NoteShape {
|
|
54
55
|
/** The byte-order mark, if the file had one. */
|
|
@@ -91,6 +92,22 @@ export interface Split {
|
|
|
91
92
|
readonly bodyStart: number;
|
|
92
93
|
/** The framing to restore when writing this note back. */
|
|
93
94
|
readonly shape: NoteShape;
|
|
95
|
+
/** `true` when the note opens a frontmatter block that never closes: it
|
|
96
|
+
* starts with a valid opening fence (`---` and a line break, after an
|
|
97
|
+
* optional BOM) and no closing `---` line follows. `yaml` is still `null`
|
|
98
|
+
* and `body` the whole text, as for a note with no block, so a projection
|
|
99
|
+
* keeps going; a caller that holds authored notes to a contract can read
|
|
100
|
+
* this to report the note as malformed instead. `false` otherwise.
|
|
101
|
+
* Optional so a `Split` built by hand, such as a test fake, still type-checks;
|
|
102
|
+
* {@link split} always sets it. */
|
|
103
|
+
readonly unterminated?: boolean | undefined;
|
|
104
|
+
}
|
|
105
|
+
/** Options for {@link newShape}. */
|
|
106
|
+
export interface NewShapeOptions {
|
|
107
|
+
/** Line ending of both fences. Default `'\n'`. */
|
|
108
|
+
readonly eol?: OpenEol | undefined;
|
|
109
|
+
/** Whether the note starts with a byte-order mark. Default `false`. */
|
|
110
|
+
readonly bom?: boolean | undefined;
|
|
94
111
|
}
|
|
95
112
|
/** The result of a tolerant read-only split: see {@link splitLenient}. */
|
|
96
113
|
export interface LenientSplit {
|
|
@@ -105,7 +122,8 @@ export interface LenientSplit {
|
|
|
105
122
|
*
|
|
106
123
|
* Frontmatter is recognised only when the document begins with `---\n` (or
|
|
107
124
|
* `---\r\n`, or either after a BOM) and is closed by `---` alone on a line.
|
|
108
|
-
* Anything else — including a missing terminator — is body
|
|
125
|
+
* Anything else — including a missing terminator — is body; a missing
|
|
126
|
+
* terminator also sets {@link Split.unterminated}. A leading BOM is
|
|
109
127
|
* framing: it is recorded in {@link Split.shape} and excluded from both
|
|
110
128
|
* `yaml` and `body`, never duplicated into the content.
|
|
111
129
|
*/
|
|
@@ -120,6 +138,30 @@ export declare function split(src: string): Split;
|
|
|
120
138
|
* reproduces `src` byte for byte for every shape.
|
|
121
139
|
*/
|
|
122
140
|
export declare function compose(frontmatter: string | null, body: string, shape: NoteShape): string;
|
|
141
|
+
/**
|
|
142
|
+
* The shape for a new note, one with no source text to {@link split}: both
|
|
143
|
+
* fences end in `eol` and the closing fence has its line ending. It stays
|
|
144
|
+
* valid when {@link NoteShape} gains fields, which an object literal would
|
|
145
|
+
* not.
|
|
146
|
+
*
|
|
147
|
+
* `compose(yaml, body, newShape())` gives `---\n<yaml>\n---\n<body>`. The
|
|
148
|
+
* `yaml` argument is the text between the fences as {@link split} returns
|
|
149
|
+
* it: without the line break before the closing fence. A YAML serializer's
|
|
150
|
+
* output usually ends in one, so drop it first, or the block ends in a
|
|
151
|
+
* blank line. With `eol: '\r\n'`, `compose` writes the fences as CRLF and
|
|
152
|
+
* the yaml text keeps its own line endings, so end its lines in `\r\n` and
|
|
153
|
+
* the text itself in `\r`, as {@link split} returns a CRLF block.
|
|
154
|
+
*
|
|
155
|
+
* An empty `yaml` gives `---\n\n---\n`: since the closing fence keeps its
|
|
156
|
+
* line break, the block holds one blank line. It round-trips through
|
|
157
|
+
* {@link split}.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* compose('title: A', '# A\n', newShape()) // '---\ntitle: A\n---\n# A\n'
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export declare function newShape(opts?: NewShapeOptions): NoteShape;
|
|
123
165
|
/**
|
|
124
166
|
* A tolerant, read-only frontmatter read for possibly-sloppy input.
|
|
125
167
|
*
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,8 @@ const BOM = '\uFEFF';
|
|
|
34
34
|
*
|
|
35
35
|
* Frontmatter is recognised only when the document begins with `---\n` (or
|
|
36
36
|
* `---\r\n`, or either after a BOM) and is closed by `---` alone on a line.
|
|
37
|
-
* Anything else — including a missing terminator — is body
|
|
37
|
+
* Anything else — including a missing terminator — is body; a missing
|
|
38
|
+
* terminator also sets {@link Split.unterminated}. A leading BOM is
|
|
38
39
|
* framing: it is recorded in {@link Split.shape} and excluded from both
|
|
39
40
|
* `yaml` and `body`, never duplicated into the content.
|
|
40
41
|
*/
|
|
@@ -42,12 +43,13 @@ export function split(src) {
|
|
|
42
43
|
const bom = src.startsWith(BOM);
|
|
43
44
|
const rest = bom ? src.slice(BOM.length) : src;
|
|
44
45
|
const offset = bom ? BOM.length : 0;
|
|
45
|
-
const noFrontmatter = () => ({
|
|
46
|
+
const noFrontmatter = (unterminated) => ({
|
|
46
47
|
yaml: null,
|
|
47
48
|
yamlRange: { start: 0, end: 0 },
|
|
48
49
|
body: rest,
|
|
49
50
|
bodyStart: offset,
|
|
50
51
|
shape: { bom, openEol: '\n', closeEol: '\n', closeSep: true },
|
|
52
|
+
unterminated,
|
|
51
53
|
});
|
|
52
54
|
let afterMarker;
|
|
53
55
|
let markerLen;
|
|
@@ -63,7 +65,7 @@ export function split(src) {
|
|
|
63
65
|
openEol = '\r\n';
|
|
64
66
|
}
|
|
65
67
|
else {
|
|
66
|
-
return noFrontmatter();
|
|
68
|
+
return noFrontmatter(false);
|
|
67
69
|
}
|
|
68
70
|
const yamlStart = offset + markerLen;
|
|
69
71
|
// Empty frontmatter: the closing fence sits directly after the opener.
|
|
@@ -78,6 +80,7 @@ export function split(src) {
|
|
|
78
80
|
body: afterMarker.slice(len),
|
|
79
81
|
bodyStart: yamlStart + len,
|
|
80
82
|
shape: { bom, openEol, closeEol, closeSep: false },
|
|
83
|
+
unterminated: false,
|
|
81
84
|
};
|
|
82
85
|
}
|
|
83
86
|
}
|
|
@@ -88,13 +91,15 @@ export function split(src) {
|
|
|
88
91
|
body: '',
|
|
89
92
|
bodyStart: src.length,
|
|
90
93
|
shape: { bom, openEol, closeEol: '', closeSep: false },
|
|
94
|
+
unterminated: false,
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
let searchStart = 0;
|
|
94
98
|
for (;;) {
|
|
95
99
|
const found = afterMarker.indexOf('\n---', searchStart);
|
|
100
|
+
// An opening fence with no closing one.
|
|
96
101
|
if (found === -1)
|
|
97
|
-
return noFrontmatter();
|
|
102
|
+
return noFrontmatter(true);
|
|
98
103
|
const dashesStart = found + 1;
|
|
99
104
|
const afterDashes = dashesStart + 3;
|
|
100
105
|
const yamlLen = found;
|
|
@@ -123,6 +128,7 @@ export function split(src) {
|
|
|
123
128
|
body: afterMarker.slice(bodyRel),
|
|
124
129
|
bodyStart: yamlStart + bodyRel,
|
|
125
130
|
shape: { bom, openEol, closeEol, closeSep: true },
|
|
131
|
+
unterminated: false,
|
|
126
132
|
};
|
|
127
133
|
}
|
|
128
134
|
}
|
|
@@ -158,6 +164,33 @@ export function compose(frontmatter, body, shape) {
|
|
|
158
164
|
out += '---' + (shape.closeEol === '' && body !== '' ? shape.openEol : shape.closeEol) + body;
|
|
159
165
|
return out;
|
|
160
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* The shape for a new note, one with no source text to {@link split}: both
|
|
169
|
+
* fences end in `eol` and the closing fence has its line ending. It stays
|
|
170
|
+
* valid when {@link NoteShape} gains fields, which an object literal would
|
|
171
|
+
* not.
|
|
172
|
+
*
|
|
173
|
+
* `compose(yaml, body, newShape())` gives `---\n<yaml>\n---\n<body>`. The
|
|
174
|
+
* `yaml` argument is the text between the fences as {@link split} returns
|
|
175
|
+
* it: without the line break before the closing fence. A YAML serializer's
|
|
176
|
+
* output usually ends in one, so drop it first, or the block ends in a
|
|
177
|
+
* blank line. With `eol: '\r\n'`, `compose` writes the fences as CRLF and
|
|
178
|
+
* the yaml text keeps its own line endings, so end its lines in `\r\n` and
|
|
179
|
+
* the text itself in `\r`, as {@link split} returns a CRLF block.
|
|
180
|
+
*
|
|
181
|
+
* An empty `yaml` gives `---\n\n---\n`: since the closing fence keeps its
|
|
182
|
+
* line break, the block holds one blank line. It round-trips through
|
|
183
|
+
* {@link split}.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* compose('title: A', '# A\n', newShape()) // '---\ntitle: A\n---\n# A\n'
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function newShape(opts = {}) {
|
|
191
|
+
const eol = opts.eol ?? '\n';
|
|
192
|
+
return { bom: opts.bom ?? false, openEol: eol, closeEol: eol, closeSep: true };
|
|
193
|
+
}
|
|
161
194
|
/**
|
|
162
195
|
* A tolerant, read-only frontmatter read for possibly-sloppy input.
|
|
163
196
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/frontmatter-lossless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Byte-exact Markdown frontmatter framing: split a note into verbatim YAML and body with source offsets, and put it back together byte for byte. BOM, CRLF and fence bytes are kept. No dependencies.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "sksizer",
|