summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/flatmatter.ts601
-rw-r--r--src/index.ts11
-rw-r--r--src/serializers/to_json.ts10
-rw-r--r--src/serializers/to_object.test.ts32
-rw-r--r--src/serializers/to_object.ts10
5 files changed, 345 insertions, 319 deletions
diff --git a/src/flatmatter.ts b/src/flatmatter.ts
index 4bd141d..f152400 100644
--- a/src/flatmatter.ts
+++ b/src/flatmatter.ts
@@ -1,358 +1,357 @@
-import { trimChar } from "./utils.ts";
+import {trimChar} from "./utils.ts";
export type Matter = {
- [key: string]: Matter | unknown;
+ [key: string]: Matter | unknown;
};
export type ConformanceResult = {
- passed: boolean;
- error?: string;
+ passed: boolean;
+ error?: string;
};
export type ParsedValue = {
- value: unknown;
- computeActions: ComputeAction[];
+ value: unknown;
+ computeActions: ComputeAction[];
};
export type ComputeAction = {
- identifier: string;
- args: Array<unknown>;
+ identifier: string;
+ args: Array<unknown>;
};
export interface Serializer {
- serialize(parsedConfig: Matter): unknown;
+ serialize(parsedConfig: Matter): unknown;
}
export interface FlatMatterFn {
- name: string;
+ name: string;
- compute(args: unknown[]): unknown;
+ compute(...args: unknown[]): unknown;
}
export default class FlatMatter {
- private content: string;
- private parsedConfig: Matter = {};
- private functions: FlatMatterFn[];
+ private content: string;
+ private parsedConfig: Matter = {};
+ private functions: FlatMatterFn[];
+
+ constructor(content: string, functions: FlatMatterFn[] = []) {
+ this.content = content;
+ this.functions = functions;
+ this.parse();
+ }
- constructor(content: string, functions: FlatMatterFn[] = []) {
- this.content = content;
- this.functions = functions;
- this.parse();
+ private parse(): void {
+ for (const line of this.content.split(/\r?\n/)) {
+ this.parseLine(line);
+ }
+ }
- console.log("done");
- }
+ /**
+ * Parses a given line of FlatMatter.
+ *
+ * @param {string} line
+ * @returns {void}
+ */
+ private parseLine(line: string): void {
+ this.validateLineConformance(line);
- private parse(): void {
- for (const line of this.content.split(/\r?\n/)) {
- this.parseLine(line);
- }
- }
-
- /**
- * Parses a given line of FlatMatter.
- *
- * @param {string} line
- * @returns {void}
- */
- private parseLine(line: string): void {
- this.validateLineConformance(line);
-
- const keys = line.split(":")[0].trim().split(".");
- const value = line.split(":").slice(1).join(":").trim();
- const parsedValue = this.parseValue(value);
-
- if (!parsedValue) return;
-
- const config = keys.reduceRight((acc, key) => {
- return { [key]: acc };
- }, this.computeValue(parsedValue)) as Matter;
-
- this.parsedConfig = { ...this.parsedConfig, ...config };
- }
-
- private validateLineConformance(line: string): void {}
-
- private validateLineHasKeyVal(line: string): ConformanceResult {
- return {
- passed: true,
- };
- }
-
- private validateLineHasOnlyOneColonChar(line: string): ConformanceResult {
- return {
- passed: true,
- };
- }
-
- /**
- * Detects if the value is a simple value. A simple value is any
- * of the following: `"a string"`, boolean `true` or `false`, or
- * anything numeric like `12345` or `123.45`.
- *
- * @param {string} value
- * @returns {boolean}
- */
- private isSimpleValue(value: string): boolean {
- const isString = value.startsWith('"') && value.endsWith('"');
- const isBoolean = value === "true" || value === "false";
- const isNumber = !Number.isNaN(value);
-
- return isString || isBoolean || isNumber;
- }
-
- /**
- * Detects if the value is a function value. A function value is any
- * of the following:
- *
- * - A function call with arguments: `(function-name *args)`
- * - A function call by reference: `function-name`
- *
- * @param {string} value
- * @returns {boolean}
- */
- private isFunctionValue(value: string): boolean {
- const isFnCall = value.startsWith("(") && value.endsWith(")");
- const isFnReference = !!value.match(/^([a-zA-Z0-9_-]+)$/);
-
- return isFnCall || isFnReference;
- }
-
- /**
- * Detects if the value is a piped value. A piped value is a mix of
- * simple and function value parts, piped together with the forward
- * slash `/` character. For example:
- *
- * ```yaml
- * posts: (get-content "posts") / (limit 10) / only-published
- * ```
- *
- * or:
- *
- * ```yaml
- * posts: "posts" / get-content / (limit 10) / only-published
- * ```
- *
- * The result of the previous pipe gets passed to the next as a first
- * argument.
- *
- * @param {string} value
- * @returns {boolean}
- */
- private isPipedValue(value: string): boolean {
- for (const part of this.composePipedValueParts(value)) {
- if (!this.isSimpleValue(part) && !this.isFunctionValue(part)) {
- return false;
- }
- }
+ const keys = line.split(":")[0].trim().split(".");
+ const value = line.split(":").slice(1).join(":").trim();
+ const parsedValue = this.parseValue(value);
- return true;
- }
-
- /**
- * Parses a value to a `ParsedValue` object, or `null`
- * in case it could not for whatever reason.
- *
- * @param {string} value
- * @returns {ParsedValue | null}
- */
- private parseValue(value: string): ParsedValue | null {
- if (this.isSimpleValue(value)) {
- return {
- value: this.parseSimpleValue(value),
- computeActions: [],
- };
- }
+ if (!parsedValue) return;
- if (this.isFunctionValue(value)) {
- return {
- value: null,
- computeActions: [this.parseFunctionValue(value)],
- };
- }
+ const config = keys.reduceRight((acc, key) => {
+ return {[key]: acc};
+ }, this.computeValue(parsedValue)) as Matter;
- if (this.isPipedValue(value)) {
- return this.parsePipedValue(value);
+ this.parsedConfig = {...this.parsedConfig, ...config};
}
- return null;
- }
-
- /**
- * Parses the value part of a line into a simple value, like for example
- * a `string`, `number` or `boolean`.
- *
- * @param {string} value
- * @returns {string | number | boolean}
- */
- private parseSimpleValue(value: string): string | number | boolean {
- if (value === "true" || value === "false") {
- return value === "true";
+ private validateLineConformance(line: string): void {
}
- if (!Number.isNaN(parseFloat(value))) {
- return parseFloat(value);
+ private validateLineHasKeyVal(line: string): ConformanceResult {
+ return {
+ passed: true,
+ };
}
- if (!Number.isNaN(parseInt(value))) {
- return parseInt(value);
+ private validateLineHasOnlyOneColonChar(line: string): ConformanceResult {
+ return {
+ passed: true,
+ };
}
- return trimChar(value, '"');
- }
-
- /**
- * Parses the value part of a line into a Compute Action, which is
- * later executed to run the function described in FlatMatter.
- *
- * @param {string} value
- * @returns {ComputeAction}
- */
- private parseFunctionValue(value: string): ComputeAction {
- const isFn = value.startsWith("(") && value.endsWith(")");
-
- if (!isFn) {
- return {
- identifier: value,
- args: [],
- };
+ /**
+ * Detects if the value is a simple value. A simple value is any
+ * of the following: `"a string"`, boolean `true` or `false`, or
+ * anything numeric like `12345` or `123.45`.
+ *
+ * @param {string} value
+ * @returns {boolean}
+ */
+ private isSimpleValue(value: string): boolean {
+ const isString = value.startsWith('"') && value.endsWith('"');
+ const isBoolean = value === "true" || value === "false";
+ const isNumber = !isNaN(parseFloat(value));
+
+ return isString || isBoolean || isNumber;
}
- const fnName = trimChar(value, ["(", ")"]).split(" ")[0].trim();
- const fnArgs = this.parseFunctionValueArgs(value);
-
- return {
- identifier: fnName,
- args: fnArgs,
- };
- }
-
- /**
- * Parses the value part of a line into a ParsedValue, which is
- * composed out of piped parts separated by the forward slash `/` character.
- *
- * The ParsedValue will include the default value, if any, and a list of compute
- * actions which will later be executed.
- *
- * @param {string} value
- * @returns {ParsedValue}
- */
- private parsePipedValue(value: string): ParsedValue {
- const parts = this.composePipedValueParts(value);
-
- if (this.isSimpleValue(parts[0])) {
- return {
- value: this.parseSimpleValue(parts[0]),
- computeActions: parts.slice(1).map((p) => this.parseFunctionValue(p)),
- };
+ /**
+ * Detects if the value is a function value. A function value is any
+ * of the following:
+ *
+ * - A function call with arguments: `(function-name *args)`
+ * - A function call by reference: `function-name`
+ *
+ * @param {string} value
+ * @returns {boolean}
+ */
+ private isFunctionValue(value: string): boolean {
+ const isFnCall = value.startsWith("(") && value.endsWith(")");
+ const isFnReference = !!value.match(/^([a-zA-Z0-9_-]+)$/);
+
+ return isFnCall || isFnReference;
}
- return {
- value: null,
- computeActions: parts.map((p) => this.parseFunctionValue(p)),
- };
- }
-
- /**
- * Takes the entire value part of a line and, assuming it is a function value,
- * parses it into a list of arguments to be passed down to the function.
- *
- * @param {string} value
- * @returns {unknown[]}
- */
- private parseFunctionValueArgs(value: string): unknown[] {
- const parts = value
- .substring(1, value.length - 1)
- .split(" ")
- .slice(1);
-
- if (!parts.length) {
- return [];
+ /**
+ * Detects if the value is a piped value. A piped value is a mix of
+ * simple and function value parts, piped together with the forward
+ * slash `/` character. For example:
+ *
+ * ```yaml
+ * posts: (get-content "posts") / (limit 10) / only-published
+ * ```
+ *
+ * or:
+ *
+ * ```yaml
+ * posts: "posts" / get-content / (limit 10) / only-published
+ * ```
+ *
+ * The result of the previous pipe gets passed to the next as a first
+ * argument.
+ *
+ * @param {string} value
+ * @returns {boolean}
+ */
+ private isPipedValue(value: string): boolean {
+ for (const part of this.composePipedValueParts(value)) {
+ if (!this.isSimpleValue(part) && !this.isFunctionValue(part)) {
+ return false;
+ }
+ }
+
+ return true;
}
- const normalizedParts = [parts[0]];
+ /**
+ * Parses a value to a `ParsedValue` object, or `null`
+ * in case it could not for whatever reason.
+ *
+ * @param {string} value
+ * @returns {ParsedValue | null}
+ */
+ private parseValue(value: string): ParsedValue | null {
+ if (this.isSimpleValue(value)) {
+ return {
+ value: this.parseSimpleValue(value),
+ computeActions: [],
+ };
+ }
+
+ if (this.isFunctionValue(value)) {
+ return {
+ value: null,
+ computeActions: [this.parseFunctionValue(value)],
+ };
+ }
+
+ if (this.isPipedValue(value)) {
+ return this.parsePipedValue(value);
+ }
+
+ return null;
+ }
- for (let i = 1; i < parts.length; i++) {
- const untilCurrent = normalizedParts.join(" ");
- const quoteCount = untilCurrent.split('"').length - 1;
+ /**
+ * Parses the value part of a line into a simple value, like for example
+ * a `string`, `number` or `boolean`.
+ *
+ * @param {string} value
+ * @returns {string | number | boolean}
+ */
+ private parseSimpleValue(value: string): string | number | boolean {
+ if (value === "true" || value === "false") {
+ return value === "true";
+ }
+
+ if (!Number.isNaN(parseFloat(value))) {
+ return parseFloat(value);
+ }
+
+ if (!Number.isNaN(parseInt(value))) {
+ return parseInt(value);
+ }
+
+ return trimChar(value, '"');
+ }
- if (quoteCount % 2 === 0) {
- normalizedParts.push(parts[i]);
- continue;
- }
+ /**
+ * Parses the value part of a line into a Compute Action, which is
+ * later executed to run the function described in FlatMatter.
+ *
+ * @param {string} value
+ * @returns {ComputeAction}
+ */
+ private parseFunctionValue(value: string): ComputeAction {
+ const isFn = value.startsWith("(") && value.endsWith(")");
+
+ if (!isFn) {
+ return {
+ identifier: value,
+ args: [],
+ };
+ }
+
+ const fnName = trimChar(value, ["(", ")"]).split(" ")[0].trim();
+ const fnArgs = this.parseFunctionValueArgs(value);
+
+ return {
+ identifier: fnName,
+ args: fnArgs,
+ };
+ }
- const lastIndex = normalizedParts.length - 1;
- const lastPart = normalizedParts[lastIndex];
+ /**
+ * Parses the value part of a line into a ParsedValue, which is
+ * composed out of piped parts separated by the forward slash `/` character.
+ *
+ * The ParsedValue will include the default value, if any, and a list of compute
+ * actions which will later be executed.
+ *
+ * @param {string} value
+ * @returns {ParsedValue}
+ */
+ private parsePipedValue(value: string): ParsedValue {
+ const parts = this.composePipedValueParts(value);
+
+ if (this.isSimpleValue(parts[0])) {
+ return {
+ value: this.parseSimpleValue(parts[0]),
+ computeActions: parts.slice(1).map((p) => this.parseFunctionValue(p)),
+ };
+ }
+
+ return {
+ value: null,
+ computeActions: parts.map((p) => this.parseFunctionValue(p)),
+ };
+ }
- normalizedParts[lastIndex] = `${lastPart} ${parts[i]}`;
+ /**
+ * Takes the entire value part of a line and, assuming it is a function value,
+ * parses it into a list of arguments to be passed down to the function.
+ *
+ * @param {string} value
+ * @returns {unknown[]}
+ */
+ private parseFunctionValueArgs(value: string): unknown[] {
+ const parts = value
+ .substring(1, value.length - 1)
+ .split(" ")
+ .slice(1);
+
+ if (!parts.length) {
+ return [];
+ }
+
+ const normalizedParts = [parts[0]];
+
+ for (let i = 1; i < parts.length; i++) {
+ const untilCurrent = normalizedParts.join(" ");
+ const quoteCount = untilCurrent.split('"').length - 1;
+
+ if (quoteCount % 2 === 0) {
+ normalizedParts.push(parts[i]);
+ continue;
+ }
+
+ const lastIndex = normalizedParts.length - 1;
+ const lastPart = normalizedParts[lastIndex];
+
+ normalizedParts[lastIndex] = `${lastPart} ${parts[i]}`;
+ }
+
+ return normalizedParts.map((part) => this.parseSimpleValue(part));
}
- return normalizedParts.map((part) => this.parseSimpleValue(part));
- }
-
- /**
- * Takes an entire value of a line and composes it into a list
- * of piped parts.
- *
- * @param {string} value
- * @returns {string[]}
- */
- private composePipedValueParts(value: string): string[] {
- const parts = value.split(" / ");
- const normalizedParts = [parts[0]];
-
- for (let i = 1; i < parts.length; i++) {
- const untilCurrent = normalizedParts.join(" / ");
- const quoteCount = untilCurrent.split('"').length - 1;
-
- if (quoteCount % 2 === 0) {
- normalizedParts.push(parts[i]);
- continue;
- }
-
- const lastIndex = normalizedParts.length - 1;
- const lastPart = normalizedParts[lastIndex];
-
- normalizedParts[lastIndex] = `${lastPart} / ${parts[i]}`;
+ /**
+ * Takes an entire value of a line and composes it into a list
+ * of piped parts.
+ *
+ * @param {string} value
+ * @returns {string[]}
+ */
+ private composePipedValueParts(value: string): string[] {
+ const parts = value.split(" / ");
+ const normalizedParts = [parts[0]];
+
+ for (let i = 1; i < parts.length; i++) {
+ const untilCurrent = normalizedParts.join(" / ");
+ const quoteCount = untilCurrent.split('"').length - 1;
+
+ if (quoteCount % 2 === 0) {
+ normalizedParts.push(parts[i]);
+ continue;
+ }
+
+ const lastIndex = normalizedParts.length - 1;
+ const lastPart = normalizedParts[lastIndex];
+
+ normalizedParts[lastIndex] = `${lastPart} / ${parts[i]}`;
+ }
+
+ return normalizedParts;
}
- return normalizedParts;
- }
+ /**
+ * Takes ParsedValue and, optionally an initial value, and runs
+ * compute actions over it to return the final computed value.
+ *
+ * @param {ParsedValue} parsedValue
+ * @returns {unknown}
+ */
+ private computeValue(parsedValue: ParsedValue): unknown {
+ let value = parsedValue.value;
- /**
- * Takes ParsedValue and, optionally an initial value, and runs
- * compute actions over it to return the final computed value.
- *
- * @param {ParsedValue} parsedValue
- * @returns {unknown}
- */
- private computeValue(parsedValue: ParsedValue): unknown {
- let value = parsedValue.value;
+ for (const ca of parsedValue.computeActions) {
+ const fnInstance = this.functions.find((f) => f.name === ca.identifier);
- for (const ca of parsedValue.computeActions) {
- const fnInstance = this.functions.find((f) => f.name === ca.identifier);
+ if (!fnInstance) {
+ continue;
+ }
- if (!fnInstance) {
- continue;
- }
+ if (value !== null) {
+ ca.args = [value, ...ca.args];
+ }
- if (value !== null) {
- ca.args = [value, ...ca.args];
- }
+ value = fnInstance.compute(...ca.args);
+ }
- value = fnInstance.compute(ca.args);
+ return value;
}
- return value;
- }
-
- /**
- * Takes a Serializer and uses it to transform internal data
- * object to a desired output.
- *
- * @param {Serializer} serializer
- * @returns {unknown}
- */
- public serialize(serializer: Serializer): unknown {
- return serializer.serialize(this.parsedConfig);
- }
+ /**
+ * Takes a Serializer and uses it to transform internal data
+ * object to a desired output.
+ *
+ * @param {Serializer} serializer
+ * @returns {unknown}
+ */
+ public serialize(serializer: Serializer): unknown {
+ return serializer.serialize(this.parsedConfig);
+ }
}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..7ac5415
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,11 @@
+import FlatMatter, {FlatMatterFn, Serializer} from "./flatmatter.ts";
+import ToObject from "./serializers/to_object.ts";
+import ToJson from "./serializers/to_json.ts";
+
+export {
+ FlatMatter,
+ FlatMatterFn,
+ Serializer,
+ ToObject,
+ ToJson,
+} \ No newline at end of file
diff --git a/src/serializers/to_json.ts b/src/serializers/to_json.ts
index 9f7b59b..f915a88 100644
--- a/src/serializers/to_json.ts
+++ b/src/serializers/to_json.ts
@@ -1,7 +1,7 @@
-import {Matter, Serializer} from '../flatmatter.ts';
+import type { Matter, Serializer } from "../flatmatter.ts";
export default class ToJson implements Serializer {
- serialize(parsedConfig: Matter): string {
- return JSON.stringify(parsedConfig);
- }
-} \ No newline at end of file
+ serialize(parsedConfig: Matter): string {
+ return JSON.stringify(parsedConfig);
+ }
+}
diff --git a/src/serializers/to_object.test.ts b/src/serializers/to_object.test.ts
index 98b165a..9f13987 100644
--- a/src/serializers/to_object.test.ts
+++ b/src/serializers/to_object.test.ts
@@ -1,13 +1,12 @@
-import { assertEquals } from "jsr:@std/assert";
-import FlatMatter from "../flatmatter.ts";
+import FlatMatter, {type FlatMatterFn} from "../flatmatter.ts";
import ToObject from "./to_object.ts";
-Deno.test("Single-level configuration", () => {
+test("Single-level configuration", () => {
const fm = new FlatMatter(
- 'a: true\nb: false\nc: 1\nd: 12.5\nf: "some string"',
+ 'a: true\nb: false\nc: 1\nd: 12.5\nf: "some string"'
);
- assertEquals(fm.serialize(new ToObject()), {
+ expect(fm.serialize(new ToObject())).toStrictEqual({
a: true,
b: false,
c: 1,
@@ -16,12 +15,12 @@ Deno.test("Single-level configuration", () => {
});
});
-Deno.test("Two-level configuration", () => {
+test("Two-level configuration", () => {
const fm = new FlatMatter(
- 'a.a: true\nb.b: false\nc.c: 1\nd.d: 12.5\nf.f: "some string"',
+ 'a.a: true\nb.b: false\nc.c: 1\nd.d: 12.5\nf.f: "some string"'
);
- assertEquals(fm.serialize(new ToObject()), {
+ expect(fm.serialize(new ToObject())).toStrictEqual({
a: {
a: true,
},
@@ -39,3 +38,20 @@ Deno.test("Two-level configuration", () => {
},
});
});
+
+test("Simple function usage", () => {
+ class ToUpper implements FlatMatterFn {
+ name = "to-upper";
+
+ compute(input: string): unknown {
+ return input.toUpperCase();
+ }
+ }
+
+ const fm = new FlatMatter('a: (to-upper "value")', [new ToUpper()]);
+ const config = fm.serialize(new ToObject());
+
+ expect(config).toStrictEqual({
+ a: "VALUE",
+ });
+});
diff --git a/src/serializers/to_object.ts b/src/serializers/to_object.ts
index eb21d2f..1b07144 100644
--- a/src/serializers/to_object.ts
+++ b/src/serializers/to_object.ts
@@ -1,7 +1,7 @@
-import {Matter, Serializer} from '../flatmatter.ts';
+import type { Matter, Serializer } from "../flatmatter.ts";
export default class ToObject implements Serializer {
- serialize(parsedConfig: Matter): Matter {
- return parsedConfig;
- }
-} \ No newline at end of file
+ serialize(parsedConfig: Matter): Matter {
+ return parsedConfig;
+ }
+}