Skip to content

Commit

Permalink
feat: add a tiny frontmatter utility (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmjuanes authored Jul 31, 2024
1 parent feef9ef commit a39d40f
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 0 deletions.
64 changes: 64 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const tags = /\{\{|\}\}/;
const endl = "\n";
const escapedChars = {
"&": "&",
"<": "&lt;",
Expand All @@ -18,6 +19,67 @@ const parse = (v, context = {}, vars = {}) => {
return (v || "").startsWith("@") ? get(vars, v.slice(1)) : get(context, v || ".");
};

// @description tiny yaml parser
const yamlParser = (str = "") => {
const lines = str.split(endl), result = {}, levels = [{ctx: result, indent: 0}];
let level = 0, i = 0;
while (i < lines.length) {
const line = lines[i] || "";
if (!!line.trim() && !line.trim().startsWith("#")) {
const indent = (line.match(/^( *)/m)?.[0] || "").length;
while(level > 0 && indent < levels[level].indent) {
levels.pop();
level = level - 1;
}
const isArrayItem = line.trim().startsWith("-");
let [key, value] = line.trim().split(":").map(v => v.trim());
// Check if is a new item of the array
if (isArrayItem) {
key = key.replace(/^(- *)/m, "");
if (typeof value === "undefined") {
value = key;
}
else {
const newIndent = (line.slice(0, line.indexOf(key))).length;
levels[level].ctx.push({});
levels.push({ctx: levels[level].ctx[levels[level].ctx.length - 1], indent: newIndent});
level = level + 1;
}
}
// Check for empty value --> entering into a nested object or array
if (!value) {
const nextLine = lines[i + 1] || "";
const nextIndent = (nextLine.match(/^( *)/m)?.[0] || "").length;
levels[level].ctx[key] = nextLine.trim().startsWith("-") ? [] : {};
if (nextIndent > levels[level].indent) {
levels.push({ctx: levels[level].ctx[key], indent: nextIndent});
level = level + 1;
}
}
else if(value && Array.isArray(levels[level].ctx)) {
levels[level].ctx.push(JSON.parse(value));
}
else if (value) {
levels[level].ctx[key] = JSON.parse(value);
}
}
i = i + 1;
}
return result;
};

// @description tiny front-matter parser
const frontmatter = (str = "", parser = null) => {
let body = (str || "").trim(), data = {};
const matches = Array.from(body.matchAll(/^(--- *)/gm))
if (matches?.length === 2 && matches[0].index === 0) {
const front = body.substring(0 + matches[0][1].length, matches[1].index).trim();
body = body.substring(matches[1].index + matches[1][1].length).trim();
data = typeof parser === "function" ? parser(front) : yamlParser(front);
}
return {body, data};
};

const defaultHelpers = {
"each": (value, opt) => {
return (typeof value === "object" ? Object.entries(value || {}) : [])
Expand Down Expand Up @@ -111,5 +173,7 @@ const mikel = (str, context = {}, opt = {}, output = []) => {
mikel.escape = escape;
mikel.get = get;
mikel.parse = parse;
mikel.yaml = yamlParser;
mikel.frontmatter = frontmatter;

export default mikel;
98 changes: 98 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,101 @@ describe("{{=function }}", () => {
assert.equal(m(`{{=concat "Hello" "World"}}!`, {}, options), "Hello World!");
});
});

describe("utils", () => {
describe("yaml", () => {
const yaml = lines => m.yaml(lines.join("\n"));

it("should parse simple key-value yaml", () => {
const json = yaml([
`string: "hello world"`,
`boolean: true`,
`number: 123`,
]);
assert.equal(typeof json, "object");
assert.equal(json.string, "hello world");
assert.equal(json.boolean, true);
assert.equal(json.number, 123);
});

it("should parse nested objects", () => {
const json = yaml([
`nested:`,
` key1: "value1"`,
` key2: "value2"`,
` nested:`,
` key1: "value1"`,
`no-nested: "another value"`,
]);
assert.equal(typeof json.nested, "object");
assert.equal(typeof json.nested.nested, "object");
assert.equal(json.nested.key1, "value1");
assert.equal(json.nested.key2, "value2");
assert.equal(json.nested.nested.key1, "value1");
assert.equal(json["no-nested"], "another value");
});

it("should parse an array of simple values", () => {
const json = yaml([
`items:`,
` - "item1"`,
` - "item2"`,
`key: "value"`,
]);
assert.equal(typeof json.items, "object");
assert.equal(json.items.length, 2);
assert.equal(json.items[0], "item1");
assert.equal(json.items[1], "item2");
assert.equal(json.key, "value");
});

it("should parse an array of objects", () => {
const json = yaml([
`items:`,
` - key1: "value1"`,
` key2: "value2"`,
` - key1: "value3"`,
` - items:`,
` - "foo"`,
` - "bar"`,
`foo: "bar"`,
]);
assert.equal(typeof json.items, "object");
assert.equal(json.items.length, 3);
assert.equal(json.items[0].key1, "value1");
assert.equal(json.items[0].key2, "value2");
assert.equal(json.items[1].key1, "value3");
assert.equal(json.items[2].items.length, 2);
assert.equal(json.items[2].items[0], "foo");
assert.equal(json.items[2].items[1], "bar");
assert.equal(json.foo, "bar");
});
});

describe("frontmatter", () => {
const frontmatter = lines => m.frontmatter(lines.join("\n"));

it("should return empty data if no frontmatter is present", () => {
const result = frontmatter([
`Hello world`,
]);
assert.equal(result.body, "Hello world");
assert.equal(Object.keys(result.data).length, 0);
});

it("should return parsed frontmatter", () => {
const result = frontmatter([
`---`,
`key: "value"`,
`items:`,
` - "foo"`,
` - "bar"`,
`---`,
`Hello world`,
]);
assert.equal(result.body, "Hello world");
assert.equal(result.data.key, "value");
assert.equal(result.data.items[1], "bar");
});
});
});

0 comments on commit a39d40f

Please sign in to comment.