tsondb 0.20.7 → 0.20.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,12 @@ const passedArguments = parseArguments({
48
48
  },
49
49
  format: {
50
50
  name: "format",
51
+ options: {
52
+ check: {
53
+ name: "check",
54
+ type: Boolean,
55
+ },
56
+ },
51
57
  },
52
58
  name: {
53
59
  name: "name",
@@ -144,9 +150,15 @@ else {
144
150
  break;
145
151
  }
146
152
  case "format": {
147
- debug(`running command: format`);
148
153
  const db = await createDB(config, config.validationOptions, true);
149
- await db.format();
154
+ if (passedArguments.command.options?.check === true) {
155
+ debug(`running command: format --check`);
156
+ await db.checkFormat();
157
+ }
158
+ else {
159
+ debug(`running command: format`);
160
+ await db.format();
161
+ }
150
162
  break;
151
163
  }
152
164
  case "name": {
@@ -64,6 +64,26 @@ export interface ValidationOptions {
64
64
  * If set, translations will be validated according to the provided options.
65
65
  */
66
66
  checkTranslations?: TranslationValidationOptions;
67
+ /**
68
+ * Special options for validating Markdown content in the database.
69
+ */
70
+ markdown?: Partial<MarkdownValidationOptions>;
71
+ }
72
+ /**
73
+ * Special options for validating Markdown content in the database.
74
+ */
75
+ export interface MarkdownValidationOptions {
76
+ /**
77
+ * Checks for the existence of referenced entities and possibly instances in attributed strings. If the attribute names are not specified via an object, the default attribute names `entity` and `instance` will be used.
78
+ */
79
+ checkInstanceReferencesInAttributedStrings?: boolean | {
80
+ entityKey: string;
81
+ instanceKey: string;
82
+ };
83
+ /**
84
+ * If set, keys in attributed strings will be checked agains the provided list of keys or regular expression. Keys for `checkInstanceReferencesInAttributedStrings` are included automatically and do not need to be specified again.
85
+ */
86
+ checkAttributedStringKeys?: string[] | RegExp;
67
87
  }
68
88
  /**
69
89
  * Options for validating translations in the database.
@@ -114,6 +134,10 @@ export declare class TSONDB<T extends DefaultTSONDBTypes = DefaultTSONDBTypes> {
114
134
  * Formats the data on disk according to the current in-memory representation.
115
135
  */
116
136
  format(): Promise<void>;
137
+ /**
138
+ * Checks if the data on disk is formatted according to the current in-memory representation.
139
+ */
140
+ checkFormat(): Promise<void>;
117
141
  /**
118
142
  * Reloads the data from disk into memory.
119
143
  */
@@ -15,14 +15,14 @@ import {} from "./schema/dsl/index.js";
15
15
  import {} from "./schema/generatedTypeHelpers.js";
16
16
  import { isEntityDeclWithParentReference } from "./schema/guards.js";
17
17
  import { serializeNode } from "./schema/treeOperations/serialization.js";
18
- import { createReferenceValidator, validateDeclReferentialIntegrity, validateDeclStructuralIntegrity, } from "./schema/treeOperations/validation.js";
18
+ import { createEntityNameValidator, createReferenceValidator, validateDeclReferentialIntegrity, validateDeclStructuralIntegrity, } from "./schema/treeOperations/validation.js";
19
19
  import { Transaction } from "./transaction.js";
20
20
  import { checkCustomConstraintsForAllEntities } from "./utils/customConstraints.js";
21
21
  import { DatabaseInMemory } from "./utils/databaseInMemory.js";
22
22
  import { applyStepsToDisk } from "./utils/databaseOnDisk.js";
23
23
  import { getAllInstanceOverviewsByEntityName, getDisplayNameFromEntityInstance, getInstanceOverview, getInstanceOverviewsByEntityName, } from "./utils/displayName.js";
24
24
  import { countError, countErrors, getErrorMessageForDisplay, HTTPError, wrapErrorsIfAny, } from "./utils/error.js";
25
- import { getFileNameForId, writeInstance } from "./utils/files.js";
25
+ import { formatInstance, getFileNameForId, readInstance, writeInstance } from "./utils/files.js";
26
26
  import { attachGitStatusToDatabaseInMemory } from "./utils/git.js";
27
27
  import { getReferencesToInstances, isReferencedByOtherInstances, } from "./utils/references.js";
28
28
  import { checkUniqueConstraintsForAllEntities } from "./utils/unique.js";
@@ -210,11 +210,12 @@ export class TSONDB {
210
210
  if (!skipStructuralValidation) {
211
211
  if (checkReferentialIntegrity) {
212
212
  debug("Checking referential integrity ...");
213
+ const entityNameValidator = createEntityNameValidator(this.#schema.isEntityName.bind(this.#schema), true);
213
214
  const referenceValidator = createReferenceValidator(this.#schema.isEntityName.bind(this.#schema), data, true);
214
215
  const referenceErrors = onlyEntities
215
216
  .flatMap(entity => parallelizeErrors(data
216
217
  .getAllInstanceContainersOfEntity(entity.name)
217
- .map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclReferentialIntegrity(validationContext, referenceValidator, [], entity, [], instance.content)))))
218
+ .map(instance => wrapErrorsIfAny(`in file ${styleText("white", `"${this.#dataRootPath}${sep}${styleText("bold", join(entity.name, getFileNameForId(instance.id)))}"`)}`, validateDeclReferentialIntegrity(validationContext, entityNameValidator, referenceValidator, [], entity, [], instance.content)))))
218
219
  .toSorted((a, b) => a.message.localeCompare(b.message));
219
220
  if (referenceErrors.length > 0) {
220
221
  const errorCount = countErrors(referenceErrors);
@@ -287,6 +288,31 @@ export class TSONDB {
287
288
  }, true);
288
289
  debug("All data is formatted");
289
290
  }
291
+ /**
292
+ * Checks if the data on disk is formatted according to the current in-memory representation.
293
+ */
294
+ async checkFormat() {
295
+ debug("Check database formatting ...");
296
+ let counter = 0;
297
+ await this.#data.forEachInstance(async (entityName, instance) => {
298
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299
+ const entity = this.#schema.getEntity(entityName);
300
+ const instanceOnDisk = await readInstance(this.#dataRootPath, entity, instance.id);
301
+ const currentFormatted = formatInstance(entity, instance.content, this.#formatterOptions);
302
+ if (instanceOnDisk !== currentFormatted) {
303
+ console.log(`Instance ${styleText("yellow", instance.id)} of entity ${styleText("cyan", entityName)} is not formatted correctly.`);
304
+ counter++;
305
+ }
306
+ }, true);
307
+ if (counter === 0) {
308
+ console.log(styleText("green", "All data is formatted"));
309
+ }
310
+ else {
311
+ console.error(styleText("red", `${counter.toFixed()} formatting error${counter === 1 ? "" : "s"} found`, {
312
+ stream: stderr,
313
+ }));
314
+ }
315
+ }
290
316
  /**
291
317
  * Reloads the data from disk into memory.
292
318
  */
@@ -7,13 +7,15 @@ export type IdentifierToCheck = {
7
7
  name: string;
8
8
  value: unknown;
9
9
  };
10
+ export type EntityNameValidator = (entityName: unknown) => Error[];
10
11
  export type ReferenceValidator = (entityName: string, instanceId: unknown) => ReferenceError[];
11
12
  export interface ValidationContext {
12
13
  useStyling: boolean;
13
14
  validationOptions: ValidationOptions;
14
15
  }
16
+ export declare const createEntityNameValidator: (isEntityName: (name: string) => boolean, useStyling: boolean) => EntityNameValidator;
15
17
  export declare const createReferenceValidator: <EM extends AnyEntityMap>(isEntityName: (name: string) => name is Extract<keyof EM, string>, databaseInMemory: DatabaseInMemory<EM>, useStyling: boolean) => ReferenceValidator;
16
18
  export declare const validateDeclStructuralIntegrity: (helpers: ValidationContext, inDecls: Decl[], decl: Decl, typeArgs: Type[], value: unknown) => TypeError[];
17
19
  export declare const validateTypeStructuralIntegrity: (helpers: ValidationContext, inDecls: Decl[], type: Type, value: unknown) => TypeError[];
18
- export declare const validateDeclReferentialIntegrity: (helpers: ValidationContext, checkReferentialIntegrity: ReferenceValidator, inDecls: Decl[], decl: Decl, typeArgs: Type[], value: unknown) => ReferenceError[];
19
- export declare const validateTypeReferentialIntegrity: (helpers: ValidationContext, checkReferentialIntegrity: ReferenceValidator, inDecls: Decl[], type: Type, value: unknown) => ReferenceError[];
20
+ export declare const validateDeclReferentialIntegrity: (helpers: ValidationContext, checkEntityName: EntityNameValidator, checkReferentialIntegrity: ReferenceValidator, inDecls: Decl[], decl: Decl, typeArgs: Type[], value: unknown) => ReferenceError[];
21
+ export declare const validateTypeReferentialIntegrity: (helpers: ValidationContext, checkEntityName: EntityNameValidator, checkReferentialIntegrity: ReferenceValidator, inDecls: Decl[], type: Type, value: unknown) => ReferenceError[];
@@ -1,3 +1,5 @@
1
+ import { validate as validateBlockMarkdown, validateInline as validateInlineMarkdown, } from "@elyukai/markdown/validate";
2
+ import { isNotNullish } from "@elyukai/utils/nullable";
1
3
  import { assertExhaustive } from "@elyukai/utils/typeSafety";
2
4
  import { MessageError, parseMessage, validate } from "messageformat";
3
5
  import { ENUM_DISCRIMINATOR_KEY } from "../../../shared/schema/declarations/EnumDecl.js";
@@ -16,6 +18,9 @@ import { getTypeArgumentsRecord } from "../dsl/declarations/Decl.js";
16
18
  import { createEntityIdentifierType } from "../dsl/declarations/EntityDecl.js";
17
19
  import { isChildEntitiesType } from "../guards.js";
18
20
  import { resolveTypeArguments } from "./typeResolution.js";
21
+ export const createEntityNameValidator = (isEntityName, useStyling) => entityName => typeof entityName === "string" && isEntityName(entityName)
22
+ ? []
23
+ : [ReferenceError(`Invalid entity name ${json(entityName, useStyling)}`)];
19
24
  export const createReferenceValidator = (isEntityName, databaseInMemory, useStyling) => (entityName, instanceId) => isEntityName(entityName)
20
25
  ? typeof instanceId === "string" &&
21
26
  databaseInMemory.hasInstanceOfEntityById(entityName, instanceId)
@@ -86,6 +91,69 @@ const validateTranslationObjectStructuralIntegrity = (context, allKeysAreRequire
86
91
  }),
87
92
  ]);
88
93
  };
94
+ const DEFAULT_ENTITY_KEY = "entity";
95
+ const DEFAULT_INSTANCE_KEY = "instance";
96
+ const normalizeAttributedStringReferenceKeys = (option) => option === true ? { entityKey: DEFAULT_ENTITY_KEY, instanceKey: DEFAULT_INSTANCE_KEY } : option;
97
+ const createStructuralAttributedStringKeyValidator = (validationOptions) => {
98
+ const { checkInstanceReferencesInAttributedStrings: checkAttrRefs, checkAttributedStringKeys } = validationOptions.markdown ?? {};
99
+ if (checkAttributedStringKeys !== undefined) {
100
+ const generalTest = Array.isArray(checkAttributedStringKeys)
101
+ ? key => checkAttributedStringKeys.includes(key)
102
+ : key => checkAttributedStringKeys.test(key);
103
+ if (checkAttrRefs === true || typeof checkAttrRefs === "object") {
104
+ const { entityKey, instanceKey } = normalizeAttributedStringReferenceKeys(checkAttrRefs);
105
+ const additionalValidKeys = [entityKey, instanceKey];
106
+ return key => generalTest(key) || additionalValidKeys.includes(key);
107
+ }
108
+ return generalTest;
109
+ }
110
+ return undefined;
111
+ };
112
+ const createStructuralAttributedStringValidator = (validationOptions) => {
113
+ const keyValidator = createStructuralAttributedStringKeyValidator(validationOptions);
114
+ if (keyValidator === undefined) {
115
+ return undefined;
116
+ }
117
+ return attributes => Object.keys(attributes)
118
+ .map(attributeName => keyValidator(attributeName)
119
+ ? undefined
120
+ : new TypeError(`invalid attribute key "${attributeName}" in attributed string`))
121
+ .filter(isNotNullish);
122
+ };
123
+ const createReferentialAttributedStringValidator = (entityValidator, referenceValidator, validationOptions) => {
124
+ const { checkInstanceReferencesInAttributedStrings: checkAttrRefs } = validationOptions.markdown ?? {};
125
+ if (checkAttrRefs === true || typeof checkAttrRefs === "object") {
126
+ const { entityKey, instanceKey } = normalizeAttributedStringReferenceKeys(checkAttrRefs);
127
+ return attributes => {
128
+ if (entityKey in attributes) {
129
+ const entityName = attributes[entityKey];
130
+ const entityErrors = entityValidator(entityName);
131
+ if (entityErrors.length > 0) {
132
+ return entityErrors;
133
+ }
134
+ if (instanceKey in attributes && typeof entityName === "string") {
135
+ const instanceId = attributes[instanceKey];
136
+ return referenceValidator(entityName, instanceId);
137
+ }
138
+ }
139
+ return [];
140
+ };
141
+ }
142
+ return undefined;
143
+ };
144
+ const validateMarkdownString = (type, value, validator) => {
145
+ if (type.markdown !== undefined && typeof value === "string") {
146
+ switch (type.markdown) {
147
+ case "inline":
148
+ return validateInlineMarkdown(value, { validateAttributes: validator });
149
+ case "block":
150
+ return validateBlockMarkdown(value, { validateAttributes: validator });
151
+ default:
152
+ return assertExhaustive(type.markdown);
153
+ }
154
+ }
155
+ return [];
156
+ };
89
157
  export const validateTypeStructuralIntegrity = (helpers, inDecls, type, value) => {
90
158
  switch (type.kind) {
91
159
  case NodeKind.ArrayType: {
@@ -149,7 +217,10 @@ export const validateTypeStructuralIntegrity = (helpers, inDecls, type, value) =
149
217
  if (typeof value !== "string") {
150
218
  return [TypeError(`expected a string, but got ${json(value, helpers.useStyling)}`)];
151
219
  }
152
- return validateStringConstraints(type, value);
220
+ return parallelizeErrors([
221
+ ...validateStringConstraints(type, value),
222
+ ...validateMarkdownString(type, value, createStructuralAttributedStringValidator(helpers.validationOptions)),
223
+ ]);
153
224
  }
154
225
  case NodeKind.TypeArgumentType: {
155
226
  throw new TypeError(`generic argument "${type.argument.name}" has not been replaced with a concrete type`);
@@ -212,13 +283,13 @@ export const validateTypeStructuralIntegrity = (helpers, inDecls, type, value) =
212
283
  return assertExhaustive(type);
213
284
  }
214
285
  };
215
- export const validateDeclReferentialIntegrity = (helpers, checkReferentialIntegrity, inDecls, decl, typeArgs, value) => {
286
+ export const validateDeclReferentialIntegrity = (helpers, checkEntityName, checkReferentialIntegrity, inDecls, decl, typeArgs, value) => {
216
287
  switch (decl.kind) {
217
288
  case NodeKind.EntityDecl:
218
- return validateTypeReferentialIntegrity(helpers, checkReferentialIntegrity, inDecls, decl.type.value, value);
289
+ return validateTypeReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, inDecls, decl.type.value, value);
219
290
  case NodeKind.EnumDecl:
220
291
  case NodeKind.TypeAliasDecl:
221
- return validateTypeReferentialIntegrity(helpers, checkReferentialIntegrity, [...inDecls, decl], resolveTypeArguments(getTypeArgumentsRecord(decl, typeArgs), decl.type.value, [
292
+ return validateTypeReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, [...inDecls, decl], resolveTypeArguments(getTypeArgumentsRecord(decl, typeArgs), decl.type.value, [
222
293
  ...inDecls,
223
294
  decl,
224
295
  ]), value);
@@ -226,11 +297,11 @@ export const validateDeclReferentialIntegrity = (helpers, checkReferentialIntegr
226
297
  return assertExhaustive(decl);
227
298
  }
228
299
  };
229
- export const validateTypeReferentialIntegrity = (helpers, checkReferentialIntegrity, inDecls, type, value) => {
300
+ export const validateTypeReferentialIntegrity = (helpers, checkEntityName, checkReferentialIntegrity, inDecls, type, value) => {
230
301
  switch (type.kind) {
231
302
  case NodeKind.ArrayType:
232
303
  return Array.isArray(value)
233
- ? parallelizeErrors(value.map((item, index) => wrapErrorsIfAny(`at index ${key(index.toString(), helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkReferentialIntegrity, inDecls, type.items, item))))
304
+ ? parallelizeErrors(value.map((item, index) => wrapErrorsIfAny(`at index ${key(index.toString(), helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, inDecls, type.items, item))))
234
305
  : [];
235
306
  case NodeKind.ObjectType: {
236
307
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -242,7 +313,7 @@ export const validateTypeReferentialIntegrity = (helpers, checkReferentialIntegr
242
313
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
243
314
  const prop = type.properties[propName];
244
315
  if (value[propName] !== undefined) {
245
- return wrapErrorsIfAny(`at object key ${key(`"${propName}"`, helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkReferentialIntegrity, inDecls, prop.type, value[propName]));
316
+ return wrapErrorsIfAny(`at object key ${key(`"${propName}"`, helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, inDecls, prop.type, value[propName]));
246
317
  }
247
318
  return undefined;
248
319
  }));
@@ -253,12 +324,12 @@ export const validateTypeReferentialIntegrity = (helpers, checkReferentialIntegr
253
324
  case NodeKind.ReferenceIdentifierType:
254
325
  return checkReferentialIntegrity(type.entity.name, value);
255
326
  case NodeKind.IncludeIdentifierType:
256
- return validateDeclReferentialIntegrity(helpers, checkReferentialIntegrity, inDecls, type.reference, type.args, value);
327
+ return validateDeclReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, inDecls, type.reference, type.args, value);
257
328
  case NodeKind.NestedEntityMapType: {
258
329
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
259
330
  return [];
260
331
  }
261
- return parallelizeErrors(Object.keys(value).map(propName => wrapErrorsIfAny(`at nested entity map ${entity(`"${type.name}"`, helpers.useStyling)} at key ${key(`"${propName}"`, helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkReferentialIntegrity, inDecls, type.type.value, value[propName]).concat(checkReferentialIntegrity(type.secondaryEntity.name, propName)))));
332
+ return parallelizeErrors(Object.keys(value).map(propName => wrapErrorsIfAny(`at nested entity map ${entity(`"${type.name}"`, helpers.useStyling)} at key ${key(`"${propName}"`, helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, inDecls, type.type.value, value[propName]).concat(checkReferentialIntegrity(type.secondaryEntity.name, propName)))));
262
333
  }
263
334
  case NodeKind.EnumType: {
264
335
  if (typeof value !== "object" ||
@@ -274,15 +345,16 @@ export const validateTypeReferentialIntegrity = (helpers, checkReferentialIntegr
274
345
  type.values[enumCase].type !== null &&
275
346
  enumCase in value
276
347
  ? parallelizeErrors([
277
- wrapErrorsIfAny(`at enum case ${key(`"${enumCase}"`, helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkReferentialIntegrity, inDecls, type.values[enumCase].type, value[enumCase])),
348
+ wrapErrorsIfAny(`at enum case ${key(`"${enumCase}"`, helpers.useStyling)}`, validateTypeReferentialIntegrity(helpers, checkEntityName, checkReferentialIntegrity, inDecls, type.values[enumCase].type, value[enumCase])),
278
349
  ])
279
350
  : [];
280
351
  }
352
+ case NodeKind.StringType:
353
+ return validateMarkdownString(type, value, createReferentialAttributedStringValidator(checkEntityName, checkReferentialIntegrity, helpers.validationOptions));
281
354
  case NodeKind.BooleanType:
282
355
  case NodeKind.DateType:
283
356
  case NodeKind.FloatType:
284
357
  case NodeKind.IntegerType:
285
- case NodeKind.StringType:
286
358
  case NodeKind.ChildEntitiesType:
287
359
  case NodeKind.TranslationObjectType:
288
360
  return [];
@@ -35,7 +35,7 @@ export const createServer = (db, config) => {
35
35
  defaultLocales,
36
36
  locales: defaultLocales,
37
37
  homeLayoutSections,
38
- validationOptions: validationOptions ?? {},
38
+ validationOptions: { ...(validationOptions ?? {}), markdown: undefined },
39
39
  };
40
40
  app.use((req, _res, next) => {
41
41
  debug("%s %s", req.method, req.originalUrl);
@@ -3,6 +3,7 @@ import type { FormatterOptions } from "../config.ts";
3
3
  import { type EntityDecl } from "../schema/dsl/index.ts";
4
4
  export declare const getFileNameForId: (id: string) => string;
5
5
  export declare const getPathToInstance: (dataRoot: string, entityName: string, id: string) => string;
6
+ export declare const readInstance: (dataRoot: string, entity: EntityDecl, id: string) => Promise<string>;
6
7
  export declare const writeInstance: (dataRoot: string, entity: EntityDecl, id: string, instance: InstanceContent, formatterOptions: Partial<FormatterOptions> | undefined) => Promise<void>;
7
8
  export declare const deleteInstance: (dataRoot: string, entityName: string, id: string) => Promise<void>;
8
9
  export declare const formatInstance: (entity: EntityDecl, instanceContent: InstanceContent, options: Partial<FormatterOptions> | undefined) => string;
@@ -1,9 +1,12 @@
1
- import { rm, writeFile } from "node:fs/promises";
1
+ import { readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import {} from "../schema/dsl/index.js";
4
4
  import { formatValue } from "../schema/treeOperations/format.js";
5
5
  export const getFileNameForId = (id) => `${id}.json`;
6
6
  export const getPathToInstance = (dataRoot, entityName, id) => join(dataRoot, entityName, getFileNameForId(id));
7
+ export const readInstance = (dataRoot, entity, id) => readFile(getPathToInstance(dataRoot, entity.name, id), {
8
+ encoding: "utf-8",
9
+ });
7
10
  export const writeInstance = (dataRoot, entity, id, instance, formatterOptions) => writeFile(getPathToInstance(dataRoot, entity.name, id), formatInstance(entity, instance, formatterOptions), {
8
11
  encoding: "utf-8",
9
12
  });
@@ -19,9 +19,9 @@ export const validateStringConstraints = (constraints, value) => parallelizeErro
19
19
  ? TypeError(`string does not match the pattern ${pattern.toString()}`)
20
20
  : undefined;
21
21
  })(),
22
- (() => {
22
+ ...(() => {
23
23
  if (constraints.markdown === undefined) {
24
- return undefined;
24
+ return [];
25
25
  }
26
26
  switch (constraints.markdown) {
27
27
  case "inline":
@@ -3404,7 +3404,7 @@ const inlineNode = combineParsers([
3404
3404
  leafParser(defaultInlineSyntaxStartSequences)
3405
3405
  ]);
3406
3406
  const inlineMarkdown = inlineNode.many();
3407
- const _parseInlineMarkdown = (syntax, keepSyntax = false, preserveEscapes = false)=>{
3407
+ const inline_parseInlineMarkdown = (syntax, keepSyntax = false, preserveEscapes = false)=>{
3408
3408
  const results = inlineMarkdown.evalT({
3409
3409
  indentation: 0,
3410
3410
  keepSyntax,
@@ -3415,8 +3415,8 @@ const _parseInlineMarkdown = (syntax, keepSyntax = false, preserveEscapes = fals
3415
3415
  if (remaining.length > 0) throw new Error(`Failed to parse the entire string. Remaining: "${remaining}"`);
3416
3416
  return result;
3417
3417
  };
3418
- const parseInlineMarkdown = (syntax)=>_parseInlineMarkdown(syntax, false);
3419
- const parseInlineMarkdownForSyntaxHighlighting = (syntax)=>_parseInlineMarkdown(syntax, true);
3418
+ const parseInlineMarkdown = (syntax)=>inline_parseInlineMarkdown(syntax, false);
3419
+ const parseInlineMarkdownForSyntaxHighlighting = (syntax)=>inline_parseInlineMarkdown(syntax, true);
3420
3420
  const isNullish = (value)=>null == value;
3421
3421
  const isNotNullish = (value)=>!isNullish(value);
3422
3422
  const sortObjectKeysByIndex = (obj, keys)=>Object.fromEntries([
@@ -3660,8 +3660,21 @@ const tableCaptionRowSyntax = sepByKeepFlat(StateTParser.string("|#").then((star
3660
3660
  },
3661
3661
  block_asText(endSpace + newline)
3662
3662
  ]))))))), block_indentation);
3663
- const tableHeaderRow = tableRow(tableContentCellGuard.then(()=>inlineMarkdown), false);
3664
- const tableHeaderRowSyntax = tableRowSyntax(tableContentCellGuard.then(()=>inlineMarkdown), false);
3663
+ const tableHeaderRow = tableRow(tableContentCellGuard.then(()=>inlineMarkdown).htoken().then((content)=>StateTParser.lookahead(StateTParser.string("||")).then(()=>StateTParser.string("|")).many().map((span)=>({
3664
+ content,
3665
+ span: span.length > 0 ? span.length + 1 : void 0
3666
+ }))), false);
3667
+ const tableHeaderRowSyntax = tableRowSyntax(tableContentCellGuard.then(()=>inlineMarkdown.then((content)=>anySpacesT.then((trailingSpace)=>StateTParser.lookahead(StateTParser.string("||")).then(()=>StateTParser.string("|")).many().map((span)=>[
3668
+ ...content,
3669
+ block_asText(trailingSpace),
3670
+ ...span.length > 0 ? [
3671
+ {
3672
+ type: "syntax",
3673
+ blockType: "table",
3674
+ content: "|".repeat(span.length)
3675
+ }
3676
+ ] : []
3677
+ ])))), false);
3665
3678
  const tableSeparatorRow = tableRow(tableHeaderSeparatorCell.htoken());
3666
3679
  const tableSeparatorRowSyntax = tableRowSyntax(tableHeaderSeparatorCell.then((separator)=>anySpacesT.map((trailingSpaces)=>[
3667
3680
  {
@@ -3723,10 +3736,6 @@ const tableSectionPlainSeparatorRowSyntax = tableRowSyntax(tableSectionSeparator
3723
3736
  ])));
3724
3737
  const tableBodyRow = tableSectionSubheaderWithSeparatorRow.orFirstW(tableSectionPlainSeparatorRow).orFirstW(tableNormalRow);
3725
3738
  const tableBodyRowSyntax = tableSectionSubheaderWithSeparatorRowSyntax.orFirstW(tableSectionPlainSeparatorRowSyntax).orFirstW(tableNormalRowSyntax);
3726
- const mapCell = (cell)=>({
3727
- type: "tableCell",
3728
- content: trimLastNodeEnd(cell)
3729
- });
3730
3739
  const mapCellObj = (cell)=>omitUndefinedKeys({
3731
3740
  type: "tableCell",
3732
3741
  content: trimLastNodeEnd(cell.content),
@@ -3762,7 +3771,7 @@ const table = tableCaptionRow.then((caption)=>(0 === caption.length ? StateTPars
3762
3771
  columns: separators.map((cell)=>omitUndefinedKeys({
3763
3772
  alignment: getAlignmentFromSeparator(cell)
3764
3773
  })),
3765
- header: header.map(mapCell),
3774
+ header: header.map(mapCellObj),
3766
3775
  rows: sectionRows(rows)
3767
3776
  }))))));
3768
3777
  const tableSyntax = tableCaptionRowSyntax.then((caption)=>(0 === caption.length ? StateTParser.of([]) : block_indentation).then((indentBetweenCaptionAndHeader)=>tableHeaderRowSyntax.then((header)=>newlineT.then((headerNewline)=>tableSeparatorRowSyntax.then((separators)=>newlineT.then((separatorsNewline)=>sepBy1KeepFlat(tableBodyRowSyntax, newlineT.map(asSingleText)).map((rows)=>[
@@ -3790,7 +3799,7 @@ const finalBlockMarkdownSyntax = anyBlankLines.then((leadingsNewlines)=>blockMar
3790
3799
  content: "\n"
3791
3800
  } : node)
3792
3801
  ])));
3793
- const _parseBlockMarkdown = (syntax, keepSyntax = false, preserveEscapes = false)=>{
3802
+ const block_parseBlockMarkdown = (syntax, keepSyntax = false, preserveEscapes = false)=>{
3794
3803
  const results = finalBlockMarkdown.evalT({
3795
3804
  indentation: 0,
3796
3805
  keepSyntax,
@@ -3812,7 +3821,7 @@ const _parseBlockMarkdown = (syntax, keepSyntax = false, preserveEscapes = false
3812
3821
  ];
3813
3822
  return result;
3814
3823
  };
3815
- const parseBlockMarkdown = (syntax)=>_parseBlockMarkdown(syntax);
3824
+ const parseBlockMarkdown = (syntax)=>block_parseBlockMarkdown(syntax);
3816
3825
  const parseBlockMarkdownForSyntaxHighlighting = (syntax)=>{
3817
3826
  const results = finalBlockMarkdownSyntax.evalT({
3818
3827
  indentation: 0,
@@ -5444,6 +5453,17 @@ const ReferenceIdentifierTypeInput = ({ type, value, instanceNamesByEntity, disa
5444
5453
  ]
5445
5454
  });
5446
5455
  };
5456
+ const getBuilderForNode = (builder, node)=>builder[node.type];
5457
+ const renderInline = (node, builder, ...args)=>{
5458
+ const parseInner = (node, ...args)=>getBuilderForNode(builder, node)(node, parseInner, ...args);
5459
+ return parseInner(node, ...args);
5460
+ };
5461
+ const render = (node, builder, ...args)=>{
5462
+ const parseInnerInline = (node, ...args)=>getBuilderForNode(builder, node)(node, parseInnerInline, ...args);
5463
+ const parseInner = (node, ...args)=>getBuilderForNode(builder, node)(node, parseInner, parseInnerInline, ...args);
5464
+ return parseInner(node, ...args);
5465
+ };
5466
+ const passThroughInnerFlat = (node, validateInner, ...args)=>node.content.flatMap((content)=>validateInner(content, ...args));
5447
5467
  class InternalMarkdownError extends Error {
5448
5468
  constructor(message){
5449
5469
  super(message);
@@ -5471,31 +5491,79 @@ const positionInStringToColumnAndRow = (position, string)=>{
5471
5491
  column
5472
5492
  };
5473
5493
  };
5474
- const validateInline = (markdown)=>{
5494
+ const emptyErrors = ()=>[];
5495
+ const inlineValidationMap = {
5496
+ attributed: (node, validateInner, options)=>passThroughInnerFlat(node, validateInner, options).concat(options?.validateAttributes?.(node.attributes) ?? []),
5497
+ bold: passThroughInnerFlat,
5498
+ italic: passThroughInnerFlat,
5499
+ link: passThroughInnerFlat,
5500
+ superscript: passThroughInnerFlat,
5501
+ code: emptyErrors,
5502
+ footnoteRef: emptyErrors,
5503
+ text: emptyErrors
5504
+ };
5505
+ const collectInlineValidationErrors = (nodes, options)=>nodes.flatMap((node)=>renderInline(node, inlineValidationMap, options));
5506
+ const blockValidationMap = {
5507
+ container: (node, validateInner, _validateInnerInline, options)=>node.content.flatMap((content)=>validateInner(content, options)),
5508
+ definitionList: (node, validateInner, validateInnerInline, options)=>node.content.flatMap((itemGroup)=>[
5509
+ ...itemGroup.terms.flatMap((term)=>term.flatMap((content)=>validateInnerInline(content, options))),
5510
+ ...itemGroup.descriptions.flatMap((description)=>description.flatMap((content)=>validateInner(content, options)))
5511
+ ]),
5512
+ footnote: (node, validateInner, _validateInnerInline, options)=>node.content.flatMap((content)=>validateInner(content, options)),
5513
+ heading: (node, _validateInner, validateInnerInline, options)=>node.content.flatMap((content)=>validateInnerInline(content, options)),
5514
+ list: (node, validateInner, _validateInnerInline, options)=>node.content.flatMap((item)=>item.content.flatMap((content)=>validateInner(content, options))),
5515
+ paragraph: (node, _validateInner, validateInnerInline, options)=>node.content.flatMap((content)=>"break" === content.type ? [] : validateInnerInline(content, options)),
5516
+ table: (node, _validateInner, validateInnerInline, options)=>[
5517
+ ...node.caption?.flatMap((captionLine)=>captionLine.flatMap((content)=>validateInnerInline(content, options))) ?? [],
5518
+ ...node.header.flatMap((cell)=>cell.content.flatMap((content)=>validateInnerInline(content, options))),
5519
+ ...checkTableRowsAreSections(node.rows) ? node.rows.flatMap((section)=>[
5520
+ ...section.header?.flatMap((cell)=>cell.content.flatMap((content)=>validateInnerInline(content, options))) ?? [],
5521
+ ...section.rows.flatMap((row)=>row.cells.flatMap((cell)=>cell.content.flatMap((content)=>validateInnerInline(content, options))))
5522
+ ]) : node.rows.flatMap((row)=>row.cells.flatMap((cell)=>cell.content.flatMap((content)=>validateInnerInline(content, options))))
5523
+ ]
5524
+ };
5525
+ const validationMap = {
5526
+ ...inlineValidationMap,
5527
+ ...blockValidationMap
5528
+ };
5529
+ const collectBlockValidationErrors = (nodes, options)=>nodes.flatMap((node)=>render(node, validationMap, options));
5530
+ const validateInline = (markdown, options)=>{
5475
5531
  const results = inlineMarkdown.evalT({
5476
5532
  indentation: 0,
5477
5533
  keepSyntax: false,
5478
5534
  preserveEscapes: false
5479
5535
  }).parse(markdown);
5480
- if (!isNotEmpty(results)) return new InternalMarkdownError("Failed to parse inline Markdown");
5481
- const [_, remaining] = results[0];
5536
+ if (!isNotEmpty(results)) return [
5537
+ new InternalMarkdownError("Failed to parse inline Markdown")
5538
+ ];
5539
+ const [nodes, remaining] = results[0];
5482
5540
  if (remaining.length > 0) {
5483
5541
  const { row, column } = positionInStringToColumnAndRow(markdown.length - remaining.length, markdown);
5484
- return new MarkdownSyntaxError(row, column, remaining);
5542
+ return [
5543
+ new MarkdownSyntaxError(row, column, remaining)
5544
+ ];
5485
5545
  }
5546
+ if (options?.validateAttributes !== void 0) return collectInlineValidationErrors(nodes, options);
5547
+ return [];
5486
5548
  };
5487
- const validate = (markdown)=>{
5549
+ const validate = (markdown, options)=>{
5488
5550
  const results = finalBlockMarkdown.evalT({
5489
5551
  indentation: 0,
5490
5552
  keepSyntax: false,
5491
5553
  preserveEscapes: false
5492
5554
  }).parse(markdown);
5493
- if (!isNotEmpty(results)) return new InternalMarkdownError("Failed to parse Markdown");
5494
- const [_, remaining] = results[0];
5555
+ if (!isNotEmpty(results) || void 0 === results[0][0] && markdown.length > 0) return [
5556
+ new InternalMarkdownError("Failed to parse Markdown")
5557
+ ];
5558
+ const [nodes = [], remaining] = results[0];
5495
5559
  if (remaining.length > 0) {
5496
5560
  const { row, column } = positionInStringToColumnAndRow(markdown.length - remaining.length, markdown);
5497
- return new MarkdownSyntaxError(row, column, remaining);
5561
+ return [
5562
+ new MarkdownSyntaxError(row, column, remaining)
5563
+ ];
5498
5564
  }
5565
+ if (options?.validateAttributes !== void 0) return collectBlockValidationErrors(nodes, options);
5566
+ return [];
5499
5567
  };
5500
5568
  const validateStringConstraints = (constraints, value)=>parallelizeErrors([
5501
5569
  void 0 !== constraints.minLength && value.length < constraints.minLength ? RangeError(`expected a string with at least ${constraints.minLength.toString()} character${1 === constraints.minLength ? "" : "s"}, but got ${value.length.toString()} character${1 === value.length ? "" : "s"}`) : void 0,
@@ -5505,8 +5573,8 @@ const validateStringConstraints = (constraints, value)=>parallelizeErrors([
5505
5573
  const pattern = "string" == typeof constraints.pattern ? new RegExp(constraints.pattern) : constraints.pattern;
5506
5574
  return pattern.test(value) ? void 0 : TypeError(`string does not match the pattern ${pattern.toString()}`);
5507
5575
  })(),
5508
- (()=>{
5509
- if (void 0 === constraints.markdown) return;
5576
+ ...(()=>{
5577
+ if (void 0 === constraints.markdown) return [];
5510
5578
  switch(constraints.markdown){
5511
5579
  case "inline":
5512
5580
  return validateInline(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsondb",
3
- "version": "0.20.7",
3
+ "version": "0.20.8",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "Lukas Obermann",
@@ -53,7 +53,7 @@
53
53
  "typescript-eslint": "^8.70.1"
54
54
  },
55
55
  "dependencies": {
56
- "@elyukai/markdown": "^0.3.4",
56
+ "@elyukai/markdown": "^0.4.0",
57
57
  "@elyukai/utils": "^0.3.6",
58
58
  "@preact/signals": "^2.11.2",
59
59
  "debug": "^4.4.3",