links-notation 0.22.0 → 0.24.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/src/Link.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { formatReference } from './ReferenceLiteral.js';
2
+
1
3
  export class Link {
2
4
  /**
3
5
  * Create a new Link
@@ -86,53 +88,7 @@ export class Link {
86
88
  return '';
87
89
  }
88
90
 
89
- // The empty reference is written as a bare delimiter pair, so that it reads
90
- // back as itself instead of disappearing from the document.
91
- if (reference === '') {
92
- return '""';
93
- }
94
-
95
- const hasSingleQuote = reference.includes("'");
96
- const hasDoubleQuote = reference.includes('"');
97
-
98
- // A reference that begins with a `#` has to be quoted, or it would read
99
- // back as a comment. A `#` anywhere else in a reference is content
100
- // (`issue#1047`), so only the first character matters.
101
- const needsQuoting =
102
- reference.startsWith('#') ||
103
- reference.includes(':') ||
104
- reference.includes('(') ||
105
- reference.includes(')') ||
106
- reference.includes(' ') ||
107
- reference.includes('\t') ||
108
- reference.includes('\n') ||
109
- reference.includes('\r') ||
110
- hasDoubleQuote ||
111
- hasSingleQuote;
112
-
113
- // Handle edge case: reference contains both single and double quotes
114
- if (hasSingleQuote && hasDoubleQuote) {
115
- // Escape single quotes and wrap in single quotes
116
- return `'${reference.replace(/'/g, "\\'")}'`;
117
- }
118
-
119
- // Prefer single quotes if double quotes are present
120
- if (hasDoubleQuote) {
121
- return `'${reference}'`;
122
- }
123
-
124
- // Use double quotes if single quotes are present
125
- if (hasSingleQuote) {
126
- return `"${reference}"`;
127
- }
128
-
129
- // Use single quotes for special characters
130
- if (needsQuoting) {
131
- return `'${reference}'`;
132
- }
133
-
134
- // No quoting needed
135
- return reference;
91
+ return formatReference(reference);
136
92
  }
137
93
 
138
94
  /**
@@ -0,0 +1,66 @@
1
+ /* global TextEncoder, TextDecoder */
2
+
3
+ /** Detect the reserved versioned-literal prefix at a reference boundary. */
4
+ export function hasReferenceLiteralPrefix(text, start = 0) {
5
+ return /^~[0-9]+\{/.test(text.slice(start));
6
+ }
7
+
8
+ /** Encode exact Unicode text as a version 1 UTF-8 hexadecimal reference. */
9
+ export function encodeReferenceLiteral(text) {
10
+ if (typeof text !== 'string' || !text.isWellFormed()) {
11
+ throw new TypeError('Reference must be well-formed Unicode text');
12
+ }
13
+ const hex = Array.from(new TextEncoder().encode(text), (byte) =>
14
+ byte.toString(16).padStart(2, '0')
15
+ ).join('');
16
+ return `~1{${hex}}`;
17
+ }
18
+
19
+ /** Decode a complete versioned literal, rejecting malformed bytes and versions. */
20
+ export function decodeReferenceLiteral(literal) {
21
+ const match = /^~([0-9]+)\{([0-9a-fA-F]*)\}$/.exec(literal);
22
+ if (
23
+ !match ||
24
+ match[0] !== literal ||
25
+ match[1] !== '1' ||
26
+ match[2].length % 2 !== 0
27
+ ) {
28
+ throw new TypeError(
29
+ 'Invalid or unsupported reference literal (expected ~1{UTF-8 hex})'
30
+ );
31
+ }
32
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(
33
+ Uint8Array.from(match[2].match(/../g) ?? [], (hex) => parseInt(hex, 16))
34
+ );
35
+ }
36
+
37
+ /** Use readable legacy quoting where possible and a literal for empty/control text. */
38
+ export function formatReference(text) {
39
+ if (typeof text !== 'string' || !text.isWellFormed()) {
40
+ throw new TypeError('Reference must be well-formed Unicode text');
41
+ }
42
+ if (!text || /[\u0000-\u001f\u007f]/u.test(text)) {
43
+ return encodeReferenceLiteral(text);
44
+ }
45
+ if (
46
+ !text.startsWith('#') &&
47
+ !/^~[0-9]+\{/.test(text) &&
48
+ !/[\p{White_Space}\ufeff():"'`]/u.test(text)
49
+ ) {
50
+ return text;
51
+ }
52
+ let choice = null;
53
+ for (const quote of ["'", '"', '`']) {
54
+ if (text.startsWith(quote)) continue;
55
+ let longest = 0;
56
+ let run = 0;
57
+ for (const character of text) {
58
+ run = character === quote ? run + 1 : 0;
59
+ longest = Math.max(longest, run);
60
+ }
61
+ const count = (longest + 1) | 1;
62
+ if (choice === null || count < choice.count) choice = { quote, count };
63
+ }
64
+ const delimiter = choice.quote.repeat(choice.count);
65
+ return delimiter + text + delimiter;
66
+ }
package/src/grammar.pegjs CHANGED
@@ -1,5 +1,6 @@
1
1
  {{
2
2
  import { DelimitedReferences } from './quotes.js';
3
+ import { decodeReferenceLiteral, hasReferenceLiteralPrefix } from './ReferenceLiteral.js';
3
4
  }}
4
5
 
5
6
  {
@@ -217,9 +218,18 @@ indentedIdLink = id:reference __ ":" eol { return { id: id, values: [] }; }
217
218
 
218
219
  // Reference can be quoted (with any number of quotes N >= 1) or simple unquoted
219
220
  // Universal approach: use procedural parsing for all quote types and counts
220
- reference = quotedReference / simpleReference
221
+ reference = referenceLiteral / quotedReference / simpleReference
222
+
223
+ // Versioned literals reserve ~<version>{<UTF-8 hex>} at a reference boundary.
224
+ // A malformed reserved token must fail rather than fall back to a bare name.
225
+ referenceLiteral = &'~' &{
226
+ return hasReferenceLiteralPrefix(input, offset());
227
+ } literal:$('~' [0-9]+ '{' [^} \t\r\n()]* '}') {
228
+ return decodeReferenceLiteral(literal);
229
+ }
221
230
 
222
- simpleReference = chars:referenceSymbol+ { return chars.join(''); }
231
+ simpleReference = &{ return !hasReferenceLiteralPrefix(input, offset()); }
232
+ chars:referenceSymbol+ { return chars.join(''); }
223
233
 
224
234
  // Universal quoted reference - handles any N quotes for all quote types
225
235
  // Uses procedural parsing with input/offset() for clean, simple logic
package/src/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  export { Link, formatLinks } from './Link.js';
2
+ export {
3
+ encodeReferenceLiteral,
4
+ decodeReferenceLiteral,
5
+ } from './ReferenceLiteral.js';
2
6
  export { LinksGroup } from './LinksGroup.js';
3
7
  export { Parser, DEFAULT_MAX_DEPTH } from './Parser.js';
4
8
  export { StreamParseError, StreamParser } from './StreamParser.js';
@@ -6,3 +10,15 @@ export { ParseError } from './ParseError.js';
6
10
  export { FormatConfig } from './FormatConfig.js';
7
11
  export { FormatOptions } from './FormatOptions.js';
8
12
  export { stripComments } from './comments.js';
13
+ export {
14
+ ArityRange,
15
+ BinaryLinoCodec,
16
+ BinaryLinoOptions,
17
+ DecodeLimits,
18
+ External,
19
+ LinksPacket,
20
+ PacketReader,
21
+ Section,
22
+ formatBinaryDocument,
23
+ formatBinaryReference,
24
+ } from './Binary.js';