correctly 0.0.0 → 0.1.0

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.
Files changed (68) hide show
  1. package/AGENTS.md +6 -2
  2. package/README.md +21 -15
  3. package/dist/ajv.d.mts +11 -0
  4. package/dist/ajv.d.mts.map +1 -0
  5. package/dist/ajv.mjs +221 -0
  6. package/dist/ajv.mjs.map +1 -0
  7. package/dist/check-BI7U3ZLc.mjs +88 -0
  8. package/dist/check-BI7U3ZLc.mjs.map +1 -0
  9. package/dist/cli.mjs +14 -62
  10. package/dist/cli.mjs.map +1 -1
  11. package/dist/config-Bx1-XXeo.d.mts +189 -0
  12. package/dist/config-Bx1-XXeo.d.mts.map +1 -0
  13. package/dist/config-Cb6o4YnJ.mjs +348 -0
  14. package/dist/config-Cb6o4YnJ.mjs.map +1 -0
  15. package/dist/core.d.mts +27 -125
  16. package/dist/core.d.mts.map +1 -1
  17. package/dist/core.mjs +5 -2
  18. package/dist/diagnostics-BqD1WOy4.mjs +70 -0
  19. package/dist/diagnostics-BqD1WOy4.mjs.map +1 -0
  20. package/dist/engine-VST0hoSk.mjs +111 -0
  21. package/dist/engine-VST0hoSk.mjs.map +1 -0
  22. package/dist/extensions-ClclWslY.d.mts +11 -0
  23. package/dist/extensions-ClclWslY.d.mts.map +1 -0
  24. package/dist/hints-HvSIhuMM.mjs +57 -0
  25. package/dist/hints-HvSIhuMM.mjs.map +1 -0
  26. package/dist/lsp.mjs +40 -91
  27. package/dist/lsp.mjs.map +1 -1
  28. package/dist/renovate.d.mts +5 -0
  29. package/dist/renovate.d.mts.map +1 -0
  30. package/dist/renovate.mjs +11 -0
  31. package/dist/renovate.mjs.map +1 -0
  32. package/dist/schemas-UxCtIdo2.mjs +265 -0
  33. package/dist/schemas-UxCtIdo2.mjs.map +1 -0
  34. package/dist/session-DCFav56x.mjs +113 -0
  35. package/dist/session-DCFav56x.mjs.map +1 -0
  36. package/dist/worker.mjs +80 -0
  37. package/dist/worker.mjs.map +1 -0
  38. package/docs/guide.md +166 -34
  39. package/examples/basic/correctly.config.ts +13 -0
  40. package/package.json +69 -60
  41. package/src/cli/check.ts +41 -11
  42. package/src/cli/main.ts +3 -2
  43. package/src/cli/readable.ts +24 -5
  44. package/src/cli/vsix.ts +1 -0
  45. package/src/core/adapters.ts +54 -0
  46. package/src/core/config.ts +201 -64
  47. package/src/core/engine.ts +118 -218
  48. package/src/core/extensions.ts +144 -0
  49. package/src/core/index.ts +3 -0
  50. package/src/core/parse.ts +26 -2
  51. package/src/core/parsers.ts +22 -0
  52. package/src/core/schema-walk.ts +43 -0
  53. package/src/core/schemas.ts +21 -41
  54. package/src/core/types.ts +33 -6
  55. package/src/extensions/renovate.ts +4 -0
  56. package/src/lsp/hints.ts +22 -12
  57. package/src/lsp/server.ts +9 -2
  58. package/src/lsp/workspace.ts +49 -54
  59. package/src/runtime/session.ts +173 -0
  60. package/src/runtime/worker.ts +99 -0
  61. package/src/validators/ajv.ts +257 -0
  62. package/src/vscode/README.md +1 -1
  63. package/src/vscode/extension.ts +2 -4
  64. package/src/vscode/package.json +9 -2
  65. package/dist/engine-BgWBnb1A.mjs +0 -746
  66. package/dist/engine-BgWBnb1A.mjs.map +0 -1
  67. package/examples/basic/correctly.config.json +0 -10
  68. package/src/core/config.schema.json +0 -47
@@ -1,247 +1,147 @@
1
- import {
2
- Ajv,
3
- type ErrorObject,
4
- type ValidateFunction,
5
- type SchemaObject,
6
- } from "ajv"
7
- import { Ajv2020 } from "ajv/dist/2020.js"
8
- import addFormats from "ajv-formats"
9
- import {
10
- associationFor,
11
- isIncluded,
12
- modeFor,
13
- schemaUri,
14
- type Project,
15
- } from "./config.ts"
16
- import {
17
- diagnostic,
18
- parseDocument,
19
- pointer,
20
- type ParsedDocument,
21
- } from "./parse.ts"
22
- import { DIALECTS, SchemaStore, type StoreOptions } from "./schemas.ts"
23
- import { validationContexts } from "./diagnostics.ts"
24
- import {
25
- CorrectlyError,
26
- failure,
27
- type Diagnostic,
28
- type FileResult,
29
- } from "./types.ts"
1
+ import { associationFor, isIncluded, readText, type Project } from "./config.ts"
2
+ import { defaultParser } from "./parsers.ts"
3
+ import { CorrectlyError, failure, type FileResult } from "./types.ts"
4
+ import type { StoreOptions } from "./schemas.ts"
5
+ import type {
6
+ PreparedValidator,
7
+ ValidationContext,
8
+ Validator,
9
+ } from "./adapters.ts"
30
10
 
31
- const ANNOTATIONS = [
32
- "markdownDescription",
33
- "enumDescriptions",
34
- "markdownEnumDescriptions",
35
- "defaultSnippets",
36
- "errorMessage",
37
- "patternErrorMessage",
38
- "deprecationMessage",
39
- "suggestSortText",
40
- "doNotSuggest",
41
- "allowComments",
42
- "allowTrailingCommas",
43
- "deprecated",
44
- "$vocabulary",
45
- ]
46
-
47
- function registerRootAnchors(ajv: Ajv, schema: unknown, uri: string) {
48
- if (typeof schema !== "object" || schema === null) return
49
- const object = schema as SchemaObject
50
- for (const anchor of [object.$anchor, object.$dynamicAnchor]) {
51
- if (typeof anchor === "string")
52
- ajv.refs[new URL(`#${anchor}`, object.$id ?? uri).href] =
53
- object.$id ?? uri
54
- }
11
+ export type EngineOptions = StoreOptions & {
12
+ onDependency?: (uri: string) => void
55
13
  }
56
-
57
- function validationMessage(error: ErrorObject): string {
58
- if (error.keyword === "anyOf")
59
- return "must match at least one alternative; none matched"
60
- if (
61
- error.keyword === "if" &&
62
- ["then", "else"].includes(error.params.failingKeyword as string)
63
- )
64
- return `must satisfy the ${error.params.failingKeyword} branch of the conditional schema`
65
- if (error.keyword === "uniqueItems") {
66
- const { i, j } = error.params
67
- if (Number.isInteger(i) && Number.isInteger(j))
68
- return `duplicates item at ${error.instancePath}${pointer([Math.min(i, j)])}; array items must be unique`
69
- }
70
- if (error.keyword === "enum" && Array.isArray(error.params.allowedValues))
71
- return `must be one of: ${error.params.allowedValues.map((value: unknown) => JSON.stringify(value)).join(", ")}`
72
- if (error.keyword === "const" && Object.hasOwn(error.params, "allowedValue"))
73
- return `must equal ${JSON.stringify(error.params.allowedValue)}`
74
- if (error.keyword === "oneOf") {
75
- if (error.params.passingSchemas === null)
76
- return "must match exactly one alternative; none matched"
77
- if (Array.isArray(error.params.passingSchemas))
78
- return `must match exactly one alternative; multiple matched (alternatives ${error.params.passingSchemas.map((index: number) => index + 1).join(", ")})`
79
- }
80
- return error.message ?? "Schema violation"
81
- }
82
-
83
- function validationDiagnostic(
84
- text: string,
85
- parsed: ParsedDocument,
86
- error: ErrorObject,
87
- ): Diagnostic {
88
- let jsonPointer = error.instancePath
89
- if (
90
- error.keyword === "uniqueItems" &&
91
- Number.isInteger(error.params.i) &&
92
- Number.isInteger(error.params.j)
93
- )
94
- jsonPointer += pointer([Math.max(error.params.i, error.params.j)])
95
- const property: unknown =
96
- error.params.additionalProperty ??
97
- error.params.unevaluatedProperty ??
98
- error.params.missingProperty ??
99
- error.params.propertyName ??
100
- error.propertyName
101
- if (typeof property === "string") jsonPointer += pointer([property])
102
- const node = parsed.locate(
103
- jsonPointer,
104
- error.keyword === "additionalProperties" ||
105
- error.keyword === "unevaluatedProperties" ||
106
- error.keyword === "propertyNames" ||
107
- typeof error.propertyName === "string",
108
- )
109
- const rangeNode =
110
- error.keyword === "required" && error.propertyName === undefined
111
- ? parsed.locate(error.instancePath)
112
- : node
113
- const length =
114
- error.keyword === "required" && error.propertyName === undefined
115
- ? 1
116
- : rangeNode?.length
117
- return diagnostic(
118
- text,
119
- `schema/${error.keyword}`,
120
- `${jsonPointer || "/"}: ${validationMessage(error)}`,
121
- jsonPointer,
122
- rangeNode?.offset ?? 0,
123
- length ?? 1,
124
- )
125
- }
126
-
127
14
  export class Engine {
128
15
  readonly project: Project
129
- readonly store: SchemaStore
130
- private readonly compiled = new Map<string, Promise<ValidateFunction>>()
131
- constructor(project: Project, options: StoreOptions = {}) {
16
+ readonly context: ValidationContext
17
+ private readonly prepared = new Map<Validator, Promise<PreparedValidator>>()
18
+ private registration: Promise<void> | undefined
19
+ constructor(project: Project, options: EngineOptions = {}) {
132
20
  this.project = project
133
- this.store = new SchemaStore(project, options)
134
- }
135
-
136
- validator(uri: string): Promise<ValidateFunction> {
137
- let compiled = this.compiled.get(uri)
138
- if (!compiled) {
139
- compiled = this.compile(uri)
140
- this.compiled.set(uri, compiled)
21
+ const watch = (uri: string) => options.onDependency?.(uri)
22
+ this.context = {
23
+ root: project.root,
24
+ configPath: project.configPath,
25
+ remote: {
26
+ ...project.config.remote,
27
+ ...(options.offline === undefined ? {} : { offline: options.offline }),
28
+ },
29
+ read: async (uri) => {
30
+ watch(uri)
31
+ return (options.read ?? readText)(uri)
32
+ },
33
+ ...(options.fetch ? { fetch: options.fetch } : {}),
34
+ ...(options.signal ? { signal: options.signal } : {}),
35
+ watch,
36
+ services: new Map(),
141
37
  }
142
- return compiled
143
38
  }
144
-
145
- private async compile(uri: string): Promise<ValidateFunction> {
146
- try {
147
- const resource = await this.store.load(uri)
148
- const options = {
149
- allErrors: true,
150
- strictSchema: true,
151
- strictTypes: false,
152
- strictTuples: false,
153
- strictRequired: false,
154
- coerceTypes: false,
155
- useDefaults: false,
156
- removeAdditional: false,
157
- validateSchema: true,
158
- loadSchema: async (ref: string) => {
159
- const loaded = await this.store.load(ref, resource.dialect)
160
- if (loaded.dialect !== resource.dialect)
161
- throw new CorrectlyError(
162
- "unsupported-dialect",
163
- `Mixed schema dialects: ${uri} references ${ref}`,
164
- )
165
- registerRootAnchors(ajv, loaded.schema, loaded.uri)
166
- return loaded.schema as SchemaObject
167
- },
168
- }
169
- const ajv =
170
- resource.dialect === "2020-12" ? new Ajv2020(options) : new Ajv(options)
171
- // CommonJS package exports retain a callable default at runtime.
172
- const formats = addFormats as unknown as (instance: Ajv) => void
173
- formats(ajv)
174
- for (const keyword of ANNOTATIONS)
175
- if (!ajv.RULES.keywords[keyword])
176
- ajv.addKeyword({ keyword, valid: true })
177
- // Ajv indexes plain anchors during reference resolution, but does not
178
- // register the annotation keyword in its strict vocabulary.
179
- if (resource.dialect === "2020-12")
180
- ajv.addKeyword({
181
- keyword: "$anchor",
182
- schemaType: "string",
183
- valid: true,
184
- })
185
- ajv.addSchema(resource.schema, resource.uri)
186
- registerRootAnchors(ajv, resource.schema, resource.uri)
187
- const validate = await ajv.compileAsync({
188
- $schema: DIALECTS[resource.dialect],
189
- $ref: uri,
190
- })
191
- return validate as ValidateFunction
192
- } catch (error) {
193
- if (error instanceof CorrectlyError) throw error
194
- throw new CorrectlyError(
195
- "schema",
196
- `Cannot compile schema ${uri}: ${error instanceof Error ? error.message : String(error)}`,
197
- )
39
+ private register(): Promise<void> {
40
+ return (this.registration ??= (async () => {
41
+ for (const rule of this.project.config.associations)
42
+ await rule.validate?.register?.(this.context)
43
+ })())
44
+ }
45
+ private validator(validator: Validator): Promise<PreparedValidator> {
46
+ let prepared = this.prepared.get(validator)
47
+ if (!prepared) {
48
+ prepared = (async () => {
49
+ await this.register()
50
+ const result = await validator.prepare(this.context)
51
+ if (!result || typeof result.validate !== "function")
52
+ throw new CorrectlyError(
53
+ "validator",
54
+ `Validator ${validator.id} did not prepare a validate function`,
55
+ )
56
+ return result
57
+ })()
58
+ this.prepared.set(validator, prepared)
198
59
  }
60
+ return prepared
199
61
  }
200
-
201
62
  async prepare(): Promise<void> {
202
- // Register configured resources before compiling references to their IDs.
203
- for (const association of this.project.config.associations) {
204
- if (association.schema !== null)
205
- await this.store.load(schemaUri(association.schema, this.project.root))
206
- }
207
- for (const association of this.project.config.associations) {
208
- if (association.schema !== null)
209
- await this.validator(schemaUri(association.schema, this.project.root))
63
+ await this.register()
64
+ for (const rule of this.project.config.associations) {
65
+ if (rule.validate) {
66
+ if (!rule.validate.accepts.includes(rule.parse?.valueModel ?? "json"))
67
+ throw new CorrectlyError(
68
+ "adapter-incompatible",
69
+ `Parser ${rule.parse?.id ?? "json"} produces ${rule.parse?.valueModel ?? "json"}; validator ${rule.validate.id} accepts ${rule.validate.accepts.join(", ")}`,
70
+ )
71
+ await this.validator(rule.validate)
72
+ }
210
73
  }
211
74
  }
212
-
75
+ async editor(file: string) {
76
+ if (!isIncluded(this.project, file)) return undefined
77
+ const association = associationFor(this.project, file)
78
+ if (!association) return undefined
79
+ const rule = this.project.config.associations[association.index]!
80
+ const parser = rule.parse ?? defaultParser(file)
81
+ if (!rule.validate || !parser.editorLanguage) return undefined
82
+ if (!rule.validate.accepts.includes(parser.valueModel)) return undefined
83
+ const prepared = await this.validator(rule.validate)
84
+ return prepared.editor
85
+ ? {
86
+ support: prepared.editor,
87
+ language: parser.editorLanguage,
88
+ association: association.index,
89
+ }
90
+ : undefined
91
+ }
213
92
  async validate(file: string, text: string): Promise<FileResult> {
214
93
  const association = associationFor(this.project, file)
215
- const mode = association?.mode ?? modeFor(file)
94
+ const rule = association
95
+ ? this.project.config.associations[association.index]
96
+ : undefined
97
+ const parser = rule?.parse ?? defaultParser(file)
216
98
  const result: FileResult = {
217
99
  file,
218
- mode,
100
+ mode: parser.id,
219
101
  association,
220
102
  coverage: "excluded",
221
103
  diagnostics: [],
222
104
  failures: [],
223
105
  }
224
106
  if (!isIncluded(this.project, file)) return result
225
- result.coverage = association?.schema ? "schema" : "syntax-only"
226
- const parsed = parseDocument(text, mode)
227
- result.diagnostics.push(...parsed.diagnostics)
228
- if (association?.schema) {
229
- try {
230
- const validate = await this.validator(association.schema)
231
- if (parsed.diagnostics.length === 0 && !validate(parsed.value)) {
107
+ result.coverage = rule?.validate
108
+ ? association?.schema
109
+ ? "schema"
110
+ : "validated"
111
+ : "syntax-only"
112
+ try {
113
+ this.context.signal?.throwIfAborted()
114
+ const parsed = await parser.parse(text, {
115
+ file,
116
+ ...(this.context.signal ? { signal: this.context.signal } : {}),
117
+ })
118
+ result.diagnostics.push(...parsed.diagnostics)
119
+ if (rule?.validate) {
120
+ if (!rule.validate.accepts.includes(parser.valueModel))
121
+ throw new CorrectlyError(
122
+ "adapter-incompatible",
123
+ `Parser ${parser.id} produces ${parser.valueModel}; validator ${rule.validate.id} accepts ${rule.validate.accepts.join(", ")}`,
124
+ )
125
+ const validator = await this.validator(rule.validate)
126
+ if (!parsed.diagnostics.length)
232
127
  result.diagnostics.push(
233
- ...validationContexts(
234
- validate.errors ?? [],
235
- (validate.errors ?? []).map((e) =>
236
- validationDiagnostic(text, parsed, e),
237
- ),
238
- ),
128
+ ...(await validator.validate({
129
+ file,
130
+ text,
131
+ parsed,
132
+ ...(this.context.signal ? { signal: this.context.signal } : {}),
133
+ })),
239
134
  )
240
- }
241
- } catch (error) {
242
- result.failures.push(failure(error, file))
243
135
  }
136
+ } catch (error) {
137
+ result.failures.push(failure(error, file))
244
138
  }
245
139
  return result
246
140
  }
141
+ async dispose(): Promise<void> {
142
+ for (const prepared of this.prepared.values()) {
143
+ const result = await prepared.catch(() => undefined)
144
+ await result?.dispose?.()
145
+ }
146
+ }
247
147
  }
@@ -0,0 +1,144 @@
1
+ import type { Ajv, FormatDefinition } from "ajv"
2
+ import { CorrectlyError } from "./types.ts"
3
+ import { schemaChildren } from "./schema-walk.ts"
4
+
5
+ /** Synchronous assertions and annotation-only keywords. Registration is explicit. */
6
+ export type AjvExtension = {
7
+ id: string
8
+ formats?: Readonly<
9
+ Record<string, FormatDefinition<string> | FormatDefinition<number>>
10
+ >
11
+ annotations?: readonly string[]
12
+ }
13
+
14
+ type ExtensionSuggestion = {
15
+ id: string
16
+ formats: readonly string[]
17
+ guidance: string
18
+ }
19
+ // Discovery metadata only: implementations are installed by the consuming project.
20
+ export const knownExtensions: readonly ExtensionSuggestion[] = [
21
+ {
22
+ id: "@correctlyjs/schemars",
23
+ formats: [
24
+ "int",
25
+ "uint",
26
+ "int8",
27
+ "uint8",
28
+ "int16",
29
+ "uint16",
30
+ "int32",
31
+ "uint32",
32
+ "int64",
33
+ "uint64",
34
+ "int128",
35
+ "uint128",
36
+ "float",
37
+ "double",
38
+ "ip",
39
+ "phone",
40
+ "partial-date-time",
41
+ ],
42
+ guidance: `For a Schemars-generated schema, install @correctlyjs/schemars and import { schemars } from "@correctlyjs/schemars/ajv". Select the schema's reviewed Schemars version with schemars({ version: "..." }) in this Ajv validator's extensions in correctly.config.ts.`,
43
+ },
44
+ ]
45
+
46
+ export function installExtensions(
47
+ ajv: Ajv,
48
+ extensions: readonly AjvExtension[],
49
+ ): void {
50
+ const formats = new Set<string>(),
51
+ annotations = new Set<string>()
52
+ const identifiers = new Set<string>()
53
+ for (const extension of extensions) {
54
+ if (
55
+ !extension ||
56
+ typeof extension !== "object" ||
57
+ typeof extension.id !== "string" ||
58
+ !extension.id
59
+ )
60
+ throw new CorrectlyError(
61
+ "extension",
62
+ "Pass extension implementations to ajv({ extensions: [...] }); string identifiers are not implementations.",
63
+ )
64
+ const id = extension.id
65
+ if (identifiers.has(id))
66
+ throw new CorrectlyError(
67
+ "extension",
68
+ `Duplicate extension identifier: ${id}`,
69
+ )
70
+ identifiers.add(id)
71
+ for (const [name, definition] of Object.entries(extension.formats ?? {})) {
72
+ if (formats.has(name))
73
+ throw new CorrectlyError(
74
+ "extension",
75
+ `Enabled extensions both define format ${JSON.stringify(name)}.`,
76
+ )
77
+ if (definition.async || typeof definition.validate !== "function")
78
+ throw new CorrectlyError(
79
+ "extension",
80
+ `Extension ${id} must provide a synchronous validator for format ${JSON.stringify(name)}.`,
81
+ )
82
+ formats.add(name)
83
+ const validate = definition.validate as (
84
+ value: string | number,
85
+ ) => unknown
86
+ ajv.addFormat(name, {
87
+ ...definition,
88
+ validate: (value: string | number) => {
89
+ const valid = validate(value)
90
+ if (typeof valid !== "boolean")
91
+ throw new CorrectlyError(
92
+ "extension",
93
+ `Extension ${id} returned a non-boolean result for format ${JSON.stringify(name)}. Validators must be synchronous.`,
94
+ )
95
+ return valid
96
+ },
97
+ })
98
+ }
99
+ for (const keyword of extension.annotations ?? []) {
100
+ if (annotations.has(keyword) || ajv.RULES.keywords[keyword])
101
+ throw new CorrectlyError(
102
+ "extension",
103
+ `Extension ${id} conflicts with keyword ${JSON.stringify(keyword)}.`,
104
+ )
105
+ annotations.add(keyword)
106
+ ajv.addKeyword({ keyword, valid: true })
107
+ }
108
+ }
109
+ }
110
+
111
+ export function assertFormats(
112
+ ajv: Ajv,
113
+ schema: unknown,
114
+ uri: string,
115
+ registry: readonly ExtensionSuggestion[],
116
+ location = "",
117
+ ): void {
118
+ if (typeof schema !== "object" || schema === null || Array.isArray(schema))
119
+ return
120
+ const object = schema as Record<string, unknown>
121
+ if (
122
+ typeof object.format === "string" &&
123
+ (!Object.hasOwn(ajv.formats, object.format) ||
124
+ ajv.formats[object.format] === true)
125
+ ) {
126
+ const format = object.format
127
+ const extension = registry.find((entry) => entry.formats.includes(format))
128
+ const hint =
129
+ extension?.guidance ??
130
+ `Pass an extension implementing ${JSON.stringify(format)} to this Ajv validator's extensions in correctly.config.ts.`
131
+ throw new CorrectlyError(
132
+ "extension-required",
133
+ `This schema needs an extension.\nFormat ${JSON.stringify(format)} has no enabled validator; validation cannot proceed.\nSchema: ${uri}#${location}/format\n${hint}`,
134
+ {
135
+ schemaUri: uri,
136
+ schemaPointer: `${location}/format`,
137
+ format,
138
+ ...(extension ? { suggestedExtension: extension.id } : {}),
139
+ },
140
+ )
141
+ }
142
+ for (const [path, child] of schemaChildren(object))
143
+ assertFormats(ajv, child, uri, registry, location + path)
144
+ }
package/src/core/index.ts CHANGED
@@ -8,3 +8,6 @@ export {
8
8
  diagnosticDetails,
9
9
  type DiagnosticView,
10
10
  } from "./diagnostics.ts"
11
+
12
+ export * from "./adapters.ts"
13
+ export * from "./parsers.ts"
package/src/core/parse.ts CHANGED
@@ -7,13 +7,15 @@ import {
7
7
  type Node,
8
8
  type ParseError,
9
9
  } from "jsonc-parser"
10
- import type { Diagnostic, Mode, Position } from "./types.ts"
10
+ import type { Diagnostic, Position } from "./types.ts"
11
11
 
12
12
  export type SourceSpan = { offset: number; length: number }
13
13
  export type ParsedDocument = {
14
14
  value: unknown
15
15
  diagnostics: Diagnostic[]
16
16
  locate: (pointer: string, key?: boolean) => SourceSpan | undefined
17
+ /** Exact numeric lexeme when the parser can preserve it. */
18
+ rawNumber?: (pointer: string) => string | undefined
17
19
  }
18
20
  export type ParsedJsonDocument = ParsedDocument & { root: Node | undefined }
19
21
 
@@ -63,7 +65,10 @@ export function diagnostic(
63
65
  }
64
66
  }
65
67
 
66
- export function parseDocument(text: string, mode: Mode): ParsedJsonDocument {
68
+ export function parseDocument(
69
+ text: string,
70
+ mode: "json" | "jsonc",
71
+ ): ParsedJsonDocument {
67
72
  const errors: ParseError[] = []
68
73
  const root = parseTree(text, errors, {
69
74
  disallowComments: mode === "json",
@@ -134,6 +139,25 @@ export function parseDocument(text: string, mode: Mode): ParsedJsonDocument {
134
139
  root,
135
140
  value: root ? value(root) : undefined,
136
141
  diagnostics,
142
+ rawNumber: (jsonPointer) => {
143
+ const parts =
144
+ jsonPointer === ""
145
+ ? []
146
+ : jsonPointer
147
+ .slice(1)
148
+ .split("/")
149
+ .map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"))
150
+ let node = root
151
+ for (const part of parts) {
152
+ if (!node) break
153
+ node = findNodeAtLocation(node, [
154
+ node.type === "array" ? Number(part) : part,
155
+ ])
156
+ }
157
+ return node?.type === "number"
158
+ ? text.slice(node.offset, node.offset + node.length)
159
+ : undefined
160
+ },
137
161
  locate: (jsonPointer, key = false) => {
138
162
  const node = locate(root, jsonPointer, key)
139
163
  return node ? { offset: node.offset, length: node.length } : undefined
@@ -0,0 +1,22 @@
1
+ import type { Parser } from "./adapters.ts"
2
+ import { parseDocument } from "./parse.ts"
3
+
4
+ export function json(): Parser {
5
+ return {
6
+ id: "json",
7
+ valueModel: "json",
8
+ editorLanguage: "json",
9
+ parse: (text) => parseDocument(text, "json"),
10
+ }
11
+ }
12
+ export function jsonc(): Parser {
13
+ return {
14
+ id: "jsonc",
15
+ valueModel: "json",
16
+ editorLanguage: "jsonc",
17
+ parse: (text) => parseDocument(text, "jsonc"),
18
+ }
19
+ }
20
+ export function defaultParser(file: string): Parser {
21
+ return file.toLowerCase().endsWith(".jsonc") ? jsonc() : json()
22
+ }
@@ -0,0 +1,43 @@
1
+ import { pointer } from "./parse.ts"
2
+
3
+ /** Only schema positions: defaults, enums, examples and other data stay opaque. */
4
+ export function* schemaChildren(
5
+ schema: Record<string, unknown>,
6
+ ): Generator<[string, unknown]> {
7
+ for (const key of [
8
+ "additionalItems",
9
+ "additionalProperties",
10
+ "contains",
11
+ "propertyNames",
12
+ "not",
13
+ "if",
14
+ "then",
15
+ "else",
16
+ "unevaluatedProperties",
17
+ "unevaluatedItems",
18
+ "contentSchema",
19
+ ]) {
20
+ if (schema[key] !== undefined) yield [pointer([key]), schema[key]]
21
+ }
22
+ for (const key of [
23
+ "properties",
24
+ "patternProperties",
25
+ "definitions",
26
+ "$defs",
27
+ "dependentSchemas",
28
+ "dependencies",
29
+ ]) {
30
+ const map = schema[key]
31
+ if (typeof map === "object" && map !== null && !Array.isArray(map))
32
+ for (const [name, child] of Object.entries(map))
33
+ yield [pointer([key, name]), child]
34
+ }
35
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems", "items"]) {
36
+ const children = schema[key]
37
+ if (Array.isArray(children)) {
38
+ for (const [index, child] of children.entries())
39
+ yield [pointer([key, index]), child]
40
+ } else if (key === "items" && children !== undefined)
41
+ yield [pointer([key]), children]
42
+ }
43
+ }