@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 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}; do not build
50
- * one by hand. A minor release may add a field, which would break an object
51
- * literal typed as `NoteShape`.
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. A leading BOM is
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. A leading BOM is
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.0.0",
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",