From 1a908bb7aae140b5c556c6b4e903b803820429dc Mon Sep 17 00:00:00 2001 From: Asko Nõmm Date: Sun, 12 Jan 2025 04:22:10 +0200 Subject: bump --- .gitignore | 1 + deno.json | 4 + deno.lock | 18 ++ src/flatmatter.test.ts | 0 src/flatmatter.ts | 359 ++++++++++++++++++++++++++++++++++++++ src/serializers/to_json.ts | 7 + src/serializers/to_object.test.ts | 37 ++++ src/serializers/to_object.ts | 7 + src/utils.ts | 7 + 9 files changed, 440 insertions(+) create mode 100644 .gitignore create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 src/flatmatter.test.ts create mode 100644 src/flatmatter.ts create mode 100644 src/serializers/to_json.ts create mode 100644 src/serializers/to_object.test.ts create mode 100644 src/serializers/to_object.ts create mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7b7c3d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cov_profile/ diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..f1c10be --- /dev/null +++ b/deno.json @@ -0,0 +1,4 @@ +{ + "fmt": { + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..c8c59c9 --- /dev/null +++ b/deno.lock @@ -0,0 +1,18 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/assert@*": "1.0.10", + "jsr:@std/internal@^1.0.5": "1.0.5" + }, + "jsr": { + "@std/assert@1.0.10": { + "integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + } + } +} diff --git a/src/flatmatter.test.ts b/src/flatmatter.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/flatmatter.ts b/src/flatmatter.ts new file mode 100644 index 0000000..84c8b7b --- /dev/null +++ b/src/flatmatter.ts @@ -0,0 +1,359 @@ +import { trimChar } from "./utils.ts"; + +export type Matter = { + [key: string]: Matter | unknown; +}; + +export type ConformanceResult = { + passed: boolean; + error?: string; +}; + +export type ParsedValue = { + value: unknown; + computeActions: ComputeAction[]; +}; + +export type ComputeAction = { + identifier: string; + args: Array; +}; + +export interface Serializer { + serialize(parsedConfig: Matter): unknown; +} + +export interface FlatMatterFn { + name: string; + + compute(args: unknown[]): unknown; +} + +export default class FlatMatter { + private content: string; + private parsedConfig: Matter = {}; + private functions: FlatMatterFn[]; + + constructor(content: string, functions: FlatMatterFn[] = []) { + this.content = content; + this.functions = functions; + this.parse(); + + console.log("done"); + } + + 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; + } + } + + 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 (this.isFunctionValue(value)) { + return { + value: null, + computeActions: [ + this.parseFunctionValue(value), + ], + }; + } + + if (this.isPipedValue(value)) { + return this.parsePipedValue(value); + } + + 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"; + } + + if (!Number.isNaN(parseFloat(value))) { + return parseFloat(value); + } + + if (!Number.isNaN(parseInt(value))) { + return parseInt(value); + } + + return value.substring(1, value.length - 1); + } + + /** + * 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, + }; + } + + /** + * 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)), + }; + } + + /** + * 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)); + } + + /** + * 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; + } + + /** + * 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); + + if (!fnInstance) { + continue; + } + + if (value !== null) { + ca.args = [value, ...ca.args]; + } + + value = fnInstance.compute(ca.args); + } + + 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); + } +} diff --git a/src/serializers/to_json.ts b/src/serializers/to_json.ts new file mode 100644 index 0000000..9f7b59b --- /dev/null +++ b/src/serializers/to_json.ts @@ -0,0 +1,7 @@ +import {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 diff --git a/src/serializers/to_object.test.ts b/src/serializers/to_object.test.ts new file mode 100644 index 0000000..d3d2255 --- /dev/null +++ b/src/serializers/to_object.test.ts @@ -0,0 +1,37 @@ +import { assertEquals } from "jsr:@std/assert"; +import FlatMatter from "../flatmatter.ts"; +import ToObject from "./to_object.ts"; + +Deno.test("Single-level configuration", () => { + const fm = new FlatMatter("a: true\nb: false\nc: 1\nd: 12.5\nf: \"some string\"") + + assertEquals(fm.serialize(new ToObject), { + a: true, + b: false, + c: 1, + d: 12.5, + f: "some string" + }); +}); + +Deno.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\"") + + assertEquals(fm.serialize(new ToObject), { + a: { + a: true, + }, + b: { + b: false, + }, + c: { + c: 1 + }, + d: { + d: 12.5 + }, + f: { + f: "some string" + } + }); +}) \ No newline at end of file diff --git a/src/serializers/to_object.ts b/src/serializers/to_object.ts new file mode 100644 index 0000000..eb21d2f --- /dev/null +++ b/src/serializers/to_object.ts @@ -0,0 +1,7 @@ +import {Matter, Serializer} from '../flatmatter.ts'; + +export default class ToObject implements Serializer { + serialize(parsedConfig: Matter): Matter { + return parsedConfig; + } +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..444e9a0 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,7 @@ +export function trimChar(input: string, chars: string | string[]): string { + if (typeof chars === "string") { + return input; + } + + return input; +} -- cgit v1.2.3