diff options
| author | Asko Nõmm <asko@nmm.ee> | 2025-01-13 01:15:52 +0200 |
|---|---|---|
| committer | Asko Nõmm <asko@nmm.ee> | 2025-01-13 01:15:52 +0200 |
| commit | 64c537957b068f9dbee5223f9c35581bef079557 (patch) | |
| tree | 9c6e758cac905cd720849333a07e52f5f697c9ba /dist | |
| parent | fb04f7fdf55a6f70a54f116ceecbdfbaa592375f (diff) | |
bump
Diffstat (limited to 'dist')
| -rw-r--r-- | dist/index.cjs | 336 | ||||
| -rw-r--r-- | dist/index.d.cts | 146 | ||||
| -rw-r--r-- | dist/index.d.ts | 146 | ||||
| -rw-r--r-- | dist/index.js | 307 |
4 files changed, 935 insertions, 0 deletions
diff --git a/dist/index.cjs b/dist/index.cjs new file mode 100644 index 0000000..430cc82 --- /dev/null +++ b/dist/index.cjs @@ -0,0 +1,336 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/index.ts +var index_exports = {}; +__export(index_exports, { + FlatMatter: () => FlatMatter, + ToJson: () => ToJson, + ToObject: () => ToObject +}); +module.exports = __toCommonJS(index_exports); + +// src/utils.ts +function trimChar(input, char) { + if (typeof char === "string") { + char = [char]; + } + for (const c of char) { + if (input.charAt(0) === c) { + input = input.substring(1); + } + if (input.charAt(input.length - 1) === c) { + input = input.substring(0, input.length - 1); + } + } + return input; +} + +// src/flatmatter.ts +var FlatMatter = class { + content; + parsedConfig = {}; + functions; + constructor(content, functions = []) { + this.content = content; + this.functions = functions; + this.parse(); + } + parse() { + for (const line of this.content.split(/\r?\n/)) { + this.parseLine(line); + } + } + /** + * Parses a given line of FlatMatter. + * + * @param {string} line + * @returns {void} + */ + parseLine(line) { + 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)); + this.parsedConfig = { ...this.parsedConfig, ...config }; + } + validateLineConformance(line) { + } + validateLineHasKeyVal(line) { + return { + passed: true + }; + } + validateLineHasOnlyOneColonChar(line) { + 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} + */ + isSimpleValue(value) { + const isString = value.startsWith('"') && value.endsWith('"'); + const isBoolean = value === "true" || value === "false"; + const isNumber = !isNaN(parseFloat(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} + */ + isFunctionValue(value) { + 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} + */ + isPipedValue(value) { + 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} + */ + parseValue(value) { + 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} + */ + parseSimpleValue(value) { + 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, '"'); + } + /** + * 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} + */ + parseFunctionValue(value) { + 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} + */ + parsePipedValue(value) { + 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[]} + */ + parseFunctionValueArgs(value) { + 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[]} + */ + composePipedValueParts(value) { + 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} + */ + computeValue(parsedValue) { + 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} + */ + serialize(serializer) { + return serializer.serialize(this.parsedConfig); + } +}; + +// src/serializers/to_object.ts +var ToObject = class { + serialize(parsedConfig) { + return parsedConfig; + } +}; + +// src/serializers/to_json.ts +var ToJson = class { + serialize(parsedConfig) { + return JSON.stringify(parsedConfig); + } +}; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + FlatMatter, + ToJson, + ToObject +}); diff --git a/dist/index.d.cts b/dist/index.d.cts new file mode 100644 index 0000000..06dbba4 --- /dev/null +++ b/dist/index.d.cts @@ -0,0 +1,146 @@ +type Matter = { + [key: string]: Matter | unknown; +}; +interface Serializer { + serialize(parsedConfig: Matter): unknown; +} +interface FlatMatterFn { + name: string; + compute(...args: unknown[]): unknown; +} +declare class FlatMatter { + private content; + private parsedConfig; + private functions; + constructor(content: string, functions?: FlatMatterFn[]); + private parse; + /** + * Parses a given line of FlatMatter. + * + * @param {string} line + * @returns {void} + */ + private parseLine; + private validateLineConformance; + private validateLineHasKeyVal; + private validateLineHasOnlyOneColonChar; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * Takes an entire value of a line and composes it into a list + * of piped parts. + * + * @param {string} value + * @returns {string[]} + */ + private composePipedValueParts; + /** + * 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; + /** + * Takes a Serializer and uses it to transform internal data + * object to a desired output. + * + * @param {Serializer} serializer + * @returns {unknown} + */ + serialize(serializer: Serializer): unknown; +} + +declare class ToObject implements Serializer { + serialize(parsedConfig: Matter): Matter; +} + +declare class ToJson implements Serializer { + serialize(parsedConfig: Matter): string; +} + +export { FlatMatter, type FlatMatterFn, type Serializer, ToJson, ToObject }; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..06dbba4 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,146 @@ +type Matter = { + [key: string]: Matter | unknown; +}; +interface Serializer { + serialize(parsedConfig: Matter): unknown; +} +interface FlatMatterFn { + name: string; + compute(...args: unknown[]): unknown; +} +declare class FlatMatter { + private content; + private parsedConfig; + private functions; + constructor(content: string, functions?: FlatMatterFn[]); + private parse; + /** + * Parses a given line of FlatMatter. + * + * @param {string} line + * @returns {void} + */ + private parseLine; + private validateLineConformance; + private validateLineHasKeyVal; + private validateLineHasOnlyOneColonChar; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * 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; + /** + * Takes an entire value of a line and composes it into a list + * of piped parts. + * + * @param {string} value + * @returns {string[]} + */ + private composePipedValueParts; + /** + * 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; + /** + * Takes a Serializer and uses it to transform internal data + * object to a desired output. + * + * @param {Serializer} serializer + * @returns {unknown} + */ + serialize(serializer: Serializer): unknown; +} + +declare class ToObject implements Serializer { + serialize(parsedConfig: Matter): Matter; +} + +declare class ToJson implements Serializer { + serialize(parsedConfig: Matter): string; +} + +export { FlatMatter, type FlatMatterFn, type Serializer, ToJson, ToObject }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..e9ee7c6 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,307 @@ +// src/utils.ts +function trimChar(input, char) { + if (typeof char === "string") { + char = [char]; + } + for (const c of char) { + if (input.charAt(0) === c) { + input = input.substring(1); + } + if (input.charAt(input.length - 1) === c) { + input = input.substring(0, input.length - 1); + } + } + return input; +} + +// src/flatmatter.ts +var FlatMatter = class { + content; + parsedConfig = {}; + functions; + constructor(content, functions = []) { + this.content = content; + this.functions = functions; + this.parse(); + } + parse() { + for (const line of this.content.split(/\r?\n/)) { + this.parseLine(line); + } + } + /** + * Parses a given line of FlatMatter. + * + * @param {string} line + * @returns {void} + */ + parseLine(line) { + 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)); + this.parsedConfig = { ...this.parsedConfig, ...config }; + } + validateLineConformance(line) { + } + validateLineHasKeyVal(line) { + return { + passed: true + }; + } + validateLineHasOnlyOneColonChar(line) { + 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} + */ + isSimpleValue(value) { + const isString = value.startsWith('"') && value.endsWith('"'); + const isBoolean = value === "true" || value === "false"; + const isNumber = !isNaN(parseFloat(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} + */ + isFunctionValue(value) { + 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} + */ + isPipedValue(value) { + 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} + */ + parseValue(value) { + 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} + */ + parseSimpleValue(value) { + 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, '"'); + } + /** + * 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} + */ + parseFunctionValue(value) { + 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} + */ + parsePipedValue(value) { + 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[]} + */ + parseFunctionValueArgs(value) { + 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[]} + */ + composePipedValueParts(value) { + 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} + */ + computeValue(parsedValue) { + 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} + */ + serialize(serializer) { + return serializer.serialize(this.parsedConfig); + } +}; + +// src/serializers/to_object.ts +var ToObject = class { + serialize(parsedConfig) { + return parsedConfig; + } +}; + +// src/serializers/to_json.ts +var ToJson = class { + serialize(parsedConfig) { + return JSON.stringify(parsedConfig); + } +}; +export { + FlatMatter, + ToJson, + ToObject +}; |
