Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ const ast = parse(`
`);
```

## Options

### `uniqueKeys`

type `boolean`\
default: `true`

Whether key uniqueness is checked.

```js
parse("a: 1\na: 2");
// Uncaught SyntaxError [YAMLSyntaxError]: Map keys must be unique

parse("a: 1\na: 2", { uniqueKeys: false });
// {type: 'root',...}
```

## Development

```sh
Expand Down
19 changes: 12 additions & 7 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type Anchor,
type Comment,
type Node,
type ParseOptions,
type Position,
type Root,
type Tag,
Expand All @@ -16,8 +17,12 @@ export type Arrayable<T> = T | T[];
export type TestCase = TestCaseSingle | TestCaseMulti;
export type TestCaseSelector = (root: Root) => Arrayable<YamlUnistNode>;

export type TestCaseSingle = [string, TestCaseSelector];
export type TestCaseMulti = [string, TestCaseSelector[]];
export type TestCaseSingle =
| [string, TestCaseSelector]
| [string, TestCaseSelector, ParseOptions];
export type TestCaseMulti =
| [string, TestCaseSelector[]]
| [string, TestCaseSelector[], ParseOptions];

export function getFirstContent<T extends YamlUnistNode>(): (root: Root) => T;
export function getFirstContent<T extends YamlUnistNode>(root: Root): T;
Expand All @@ -37,10 +42,10 @@ export function testCases(
options: SnapshotNodeOptions = {},
) {
cases.forEach(testCase => {
const [text, selector] = testCase;
const root = parse(text);
const [text, selector, parseOptions] = testCase;
const root = parse(text, parseOptions);

testCrLf(root, text);
testCrLf(root, text, parseOptions);

const selectNodes = ([] as TestCaseSelector[]).concat(selector);
selectNodes.forEach(selectNode => {
Expand All @@ -54,11 +59,11 @@ export function testCases(
});
}

function testCrLf(lfRoot: Root, lfText: string) {
function testCrLf(lfRoot: Root, lfText: string, parseOptions?: ParseOptions) {
const crLfText = lfText.replace(/\n/g, "\r\n");

test(getTestTitle(lfText), () => {
const crLfRoot = parse(crLfText);
const crLfRoot = parse(crLfText, parseOptions);
testNode(lfRoot, crLfRoot);
});

Expand Down
19 changes: 3 additions & 16 deletions src/options.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,16 @@
import { parse } from "./parse.js";

for (const { type, text } of [
{ type: "mapping", text: "<<: 1\n<<: 2" },
{ type: "flowMapping", text: '{"<<": 1,"<<": 2}' },
]) {
test(`(${type}): duplicate '<<' keys should always be allowed`, () => {
expect(parse(text)).toBeDefined();
expect(parse(text, { allowDuplicateKeysInMap: false })).toBeDefined();
const ast = parse(text, { allowDuplicateKeysInMap: true });
expect(ast).toBeDefined();
const node = ast.children[0].children[1].children[0];
expect(node?.type).toBe(type);
});
}

for (const { type, text } of [
{ type: "mapping", text: "a: 1\na: 2" },
{ type: "flowMapping", text: `{"a":1,"a":2}` },
{ type: "flowMapping", text: `{"a":1,'a':2}` },
]) {
test(`(${type}): duplicate keys in ${text}`, () => {
expect(() => parse(text)).toThrowError(`Map keys must be unique`);
expect(() => parse(text, { allowDuplicateKeysInMap: false })).toThrowError(
expect(() => parse(text, { uniqueKeys: true })).toThrowError(
`Map keys must be unique`,
);
const ast = parse(text, { allowDuplicateKeysInMap: true });
const ast = parse(text, { uniqueKeys: false });
expect(ast).toBeDefined();
const node = ast.children[0].children[1].children[0];
expect(node?.type).toBe(type);
Expand Down
37 changes: 3 additions & 34 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import { removeFakeNodes } from "./utils/remove-fake-nodes.js";
import { updatePositions } from "./utils/update-positions.js";

export function parse(text: string, options?: ParseOptions): Root {
// const allowDuplicateKeysInMap = options?.allowDuplicateKeysInMap;
const parser = new YAML.Parser();
const composer = new YAML.Composer({
keepSourceTokens: true,
uniqueKeys: true,
// Intentionally to not cast to boolean, so user can pass a function (undocumented)
// https://eemeli.org/yaml/#options
uniqueKeys: options?.uniqueKeys,
});
const documentNodes: YAML.Document.Parsed[] = [];
const cstTokens: YAML.CST.Token[] = [];
Expand All @@ -29,12 +30,8 @@ export function parse(text: string, options?: ParseOptions): Root {
documentNodes.push(doc);
}

const allowDuplicateKeysInMap = options?.allowDuplicateKeysInMap;
for (const doc of documentNodes) {
for (const error of doc.errors) {
if (shouldIgnoreError(text, error, allowDuplicateKeysInMap)) {
continue;
}
throw transformError(error, context);
}
}
Expand All @@ -51,31 +48,3 @@ export function parse(text: string, options?: ParseOptions): Root {

return root;
}

const ERROR_CODE_DUPLICATE_KEY = "DUPLICATE_KEY";
function shouldIgnoreError(
text: string,
error: unknown,
allowDuplicateKeysInMap: boolean | undefined,
): boolean | undefined {
if (
!(
error instanceof YAML.YAMLParseError &&
error.code === ERROR_CODE_DUPLICATE_KEY
)
) {
return false;
}

if (allowDuplicateKeysInMap) {
return true;
}

const index = error.pos[0];
const character = text.charAt(index);
const key =
character === "<"
? text.slice(index, index + 2)
: text.slice(index + 1, index + 3);
return key === "<<";
}
1 change: 1 addition & 0 deletions src/transforms/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ testCases([
[
"x:\n - &a\n key1: value1\n - &b\n key2: value2\nfoo:\n bar: baz\n <<: *a\n <<: *b",
getMappingItem(1),
{ uniqueKeys: false },
],
[
"merge:\n- &A { a: 1 }\n- &B { b: 2 }\n- <<: [ *A, *B ]",
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface ParseOptions {
allowDuplicateKeysInMap?: boolean;
uniqueKeys?: boolean;
}

export interface Node {
Expand Down
Loading