├── lib-python ├── .python-version ├── setup.cfg ├── .gitignore ├── src │ └── kson │ │ └── _marker.c ├── setup.py ├── readme.md ├── uvw ├── uvw.bat ├── MANIFEST.in └── pyproject.toml ├── src ├── jsMain │ ├── kotlin │ │ └── readme.md │ └── resources │ │ └── readme.md ├── jsTest │ ├── kotlin │ │ ├── readme.md │ │ └── org │ │ │ └── kson │ │ │ └── testSupport │ │ │ └── YamlValidator.kt │ └── resources │ │ └── readme.md ├── jvmMain │ ├── kotlin │ │ └── readme.md │ └── resources │ │ └── readme.md ├── jvmTest │ ├── kotlin │ │ ├── readme.md │ │ └── org │ │ │ └── kson │ │ │ └── testSupport │ │ │ └── YamlValidator.kt │ └── resources │ │ └── readme.md ├── nativeKsonTest │ ├── readme.md │ ├── resources │ │ └── readme.md │ └── kotlin │ │ └── org │ │ └── kson │ │ └── testSupport │ │ └── YamlValidator.kt ├── nativeMain │ ├── kotlin │ │ └── readme.md │ └── resources │ │ └── readme.md ├── commonMain │ ├── resources │ │ └── readme.md │ └── kotlin │ │ └── org │ │ └── kson │ │ ├── validation │ │ └── Validator.kt │ │ ├── stdlibx │ │ ├── readme.md │ │ ├── exceptions │ │ │ ├── ShouldNotHappenException.kt │ │ │ └── FatalParseException.kt │ │ └── collections │ │ │ └── Immutable.kt │ │ ├── parser │ │ ├── behavior │ │ │ ├── readme.md │ │ │ ├── StringUnquoted.kt │ │ │ └── embedblock │ │ │ │ ├── EmbedBlockSerialization.kt │ │ │ │ └── EmbedBlockIndent.kt │ │ └── MessageSink.kt │ │ └── schema │ │ └── validators │ │ ├── AllOfValidator.kt │ │ ├── MaxItemsValidator.kt │ │ ├── MinItemsValidator.kt │ │ ├── PropertyNamesValidator.kt │ │ ├── MaximumValidator.kt │ │ ├── MinimumValidator.kt │ │ ├── MaxPropertiesValidator.kt │ │ ├── MaxLengthValidator.kt │ │ ├── MinLengthValidator.kt │ │ ├── EnumValidator.kt │ │ ├── PatternValidator.kt │ │ ├── MinPropertiesValidator.kt │ │ ├── ExclusiveMaximumValidator.kt │ │ ├── ExclusiveMinimumValidator.kt │ │ ├── NotValidator.kt │ │ ├── OneOfValidator.kt │ │ ├── ContainsValidator.kt │ │ ├── IfValidator.kt │ │ ├── RequiredValidator.kt │ │ ├── MultipleOfValidator.kt │ │ ├── UniqueItemsValidator.kt │ │ ├── RefValidator.kt │ │ ├── ConstValidator.kt │ │ └── TypeValidator.kt └── commonTest │ ├── resources │ └── readme.md │ └── kotlin │ └── org │ └── kson │ ├── testSupport │ ├── YamlValidator.kt │ └── JsonValidator.kt │ ├── schema │ ├── JsonSchemaTestNumber.kt │ ├── JsonSchemaTestArray.kt │ ├── JsonSchemaTestType.kt │ └── validators │ │ ├── ConstValidatorTest.kt │ │ ├── AllOfValidatorTest.kt │ │ └── AnyOfValidatorTest.kt │ ├── KsonCoreTestNumberError.kt │ ├── parser │ ├── json │ │ └── readme.md │ ├── messages │ │ └── MessagesTest.kt │ └── ParserTest.kt │ ├── KsonCoreTestBasicLiteral.kt │ ├── KsonCoreTestEmbedBlockError.kt │ ├── KsonCoreTestStringError.kt │ └── KsonCoreTestNumber.kt ├── tooling ├── lsp-clients │ ├── shared │ │ ├── build.gradle.kts │ │ ├── src │ │ │ ├── connection │ │ │ │ ├── index.ts │ │ │ │ └── browserConnection.ts │ │ │ ├── index.ts │ │ │ └── config │ │ │ │ └── index.ts │ │ ├── extension │ │ │ └── assets │ │ │ │ └── kson-icon.png │ │ ├── tsconfig.json │ │ └── package.json │ ├── monaco │ │ ├── .gitignore │ │ ├── src │ │ │ ├── vite-env.d.ts │ │ │ ├── worker │ │ │ │ └── ksonServer.ts │ │ │ ├── index.ts │ │ │ ├── YamlEditorManager.ts │ │ │ └── KsonEditorManager.ts │ │ ├── readme.md │ │ ├── playwright.config.ts │ │ ├── resources │ │ │ └── kson │ │ │ │ └── example.kson │ │ ├── package.json │ │ ├── vite.config.ts │ │ └── index.html │ ├── vscode │ │ ├── test │ │ │ ├── workspace │ │ │ │ └── .gitignore │ │ │ ├── test-files.json │ │ │ ├── browser │ │ │ │ └── index.browser.ts │ │ │ ├── node │ │ │ │ └── index.node.ts │ │ │ ├── mocha-config.ts │ │ │ ├── suite │ │ │ │ ├── assert.ts │ │ │ │ ├── syntax-highlighting.test.ts │ │ │ │ ├── common.ts │ │ │ │ └── editing.test.ts │ │ │ └── index.common.ts │ │ ├── src │ │ │ ├── server │ │ │ │ ├── browser │ │ │ │ │ └── ksonServerMain.ts │ │ │ │ └── node │ │ │ │ │ └── ksonServerMain.ts │ │ │ ├── client │ │ │ │ └── common │ │ │ │ │ └── deactivate.ts │ │ │ ├── stubs │ │ │ │ └── minimatch.stub.ts │ │ │ └── config │ │ │ │ └── clientOptions.ts │ │ ├── .gitignore │ │ ├── tsconfig.json │ │ ├── .vscode │ │ │ ├── tasks.json │ │ │ └── launch.json │ │ └── readme.md │ ├── gradle.properties │ ├── pixi.toml │ ├── tsconfig.json │ ├── package.json │ ├── readme.md │ └── build.gradle.kts ├── jetbrains │ ├── src │ │ ├── test │ │ │ ├── resources │ │ │ │ └── testData │ │ │ │ │ ├── parser │ │ │ │ │ ├── BlankFile.kson │ │ │ │ │ ├── Number.kson │ │ │ │ │ ├── UnopenedList.kson │ │ │ │ │ ├── NumberError.kson │ │ │ │ │ ├── Object.kson │ │ │ │ │ ├── UnclosedString.kson │ │ │ │ │ ├── NumberDanglingExponentError.kson │ │ │ │ │ ├── BlankFile.txt │ │ │ │ │ ├── EmptyCommaError.kson │ │ │ │ │ ├── UnclosedEmbedBlock.kson │ │ │ │ │ ├── TwoConsecutiveStrings.kson │ │ │ │ │ ├── DelimitedDashList.kson │ │ │ │ │ ├── Number.txt │ │ │ │ │ ├── NumberError.txt │ │ │ │ │ ├── UnopenedList.txt │ │ │ │ │ ├── UnclosedString.txt │ │ │ │ │ ├── ManyConstructs.kson │ │ │ │ │ ├── UnclosedEmbedBlock.txt │ │ │ │ │ ├── Object.txt │ │ │ │ │ ├── NumberDanglingExponentError.txt │ │ │ │ │ ├── TwoConsecutiveStrings.txt │ │ │ │ │ └── EmptyCommaError.txt │ │ │ │ │ ├── rename │ │ │ │ │ ├── foo.xml │ │ │ │ │ └── foo_after.xml │ │ │ │ │ ├── folding │ │ │ │ │ ├── singleLineShouldNotFold.kson │ │ │ │ │ ├── objectFolding.kson │ │ │ │ │ ├── dashListFolding.kson │ │ │ │ │ ├── bracketListFolding.kson │ │ │ │ │ ├── angleListFolding.kson │ │ │ │ │ └── embedBlockFolding.kson │ │ │ │ │ └── annotator │ │ │ │ │ ├── stringValues.kson │ │ │ │ │ └── objectKeys.kson │ │ │ └── kotlin │ │ │ │ └── org │ │ │ │ └── kson │ │ │ │ └── jetbrains │ │ │ │ ├── editor │ │ │ │ ├── KsonCommenterTest.kt │ │ │ │ ├── KsonQuoteMatcherTest.kt │ │ │ │ └── KsonPairedBraceMatcherTest.kt │ │ │ │ ├── highlighter │ │ │ │ └── KsonSemanticHighlightAnnotatorTest.kt │ │ │ │ ├── folding │ │ │ │ └── KsonFoldingTest.kt │ │ │ │ ├── psi │ │ │ │ └── KsonElementGeneratorTest.kt │ │ │ │ ├── parser │ │ │ │ └── KsonLexerTest.kt │ │ │ │ └── KsonPsiFileTest.kt │ │ └── main │ │ │ ├── resources │ │ │ ├── icons │ │ │ │ ├── kson_icon.png │ │ │ │ └── kson_icon@2x.png │ │ │ └── messages │ │ │ │ └── KsonBundle.properties │ │ │ └── kotlin │ │ │ └── org │ │ │ └── kson │ │ │ └── jetbrains │ │ │ ├── KsonLanguage.kt │ │ │ ├── psi │ │ │ ├── KsonPsiElement.kt │ │ │ ├── KsonPsiFile.kt │ │ │ └── KsonEmbedBlock.kt │ │ │ ├── KsonIcons.kt │ │ │ ├── config │ │ │ └── KsonCodeStyleSettings.kt │ │ │ ├── highlighter │ │ │ ├── KsonSyntaxHighlighterFactory.kt │ │ │ └── KsonSemanticHighlightAnnotator.kt │ │ │ ├── editor │ │ │ ├── KsonQuoteHandler.kt │ │ │ ├── KsonCommenter.kt │ │ │ ├── KsonCompletionContributor.kt │ │ │ ├── KsonPairedBraceMatcher.kt │ │ │ └── EmbedBlockLanguageListCompletionProvider.kt │ │ │ ├── KsonBundle.kt │ │ │ ├── file │ │ │ └── KsonFileType.kt │ │ │ ├── util │ │ │ ├── DocumentExtensions.kt │ │ │ └── PsiFileExtensions.kt │ │ │ ├── formatter │ │ │ └── KsonExternalFormatter.kt │ │ │ ├── inject │ │ │ ├── KsonLanguageInjectionContributor.kt │ │ │ └── KsonLanguageInjectionPerformer.kt │ │ │ ├── parser │ │ │ └── KsonParserDefinition.kt │ │ │ └── pages │ │ │ └── KsonColorSettingsPage.kt │ ├── gradle.properties │ └── readme.md ├── language-server-protocol │ ├── .mocharc.json │ ├── examples │ │ ├── .kson-schema.kson │ │ ├── config │ │ │ └── app.config.kson │ │ └── schemas │ │ │ └── config.schema.kson │ ├── gradle.properties │ ├── pixi.toml │ ├── src │ │ ├── core │ │ │ ├── schema │ │ │ │ ├── createSchemaProvider.browser.ts │ │ │ │ ├── createSchemaProvider.node.ts │ │ │ │ ├── SchemaProvider.ts │ │ │ │ └── SchemaConfig.ts │ │ │ ├── features │ │ │ │ ├── FormattingService.ts │ │ │ │ ├── HoverService.ts │ │ │ │ └── CodeLensService.ts │ │ │ ├── commands │ │ │ │ ├── CommandExecutorFactory.ts │ │ │ │ ├── createCommandExecutor.browser.ts │ │ │ │ ├── createCommandExecutor.node.ts │ │ │ │ ├── CommandType.ts │ │ │ │ ├── CommandExecutor.browser.ts │ │ │ │ ├── CommandParameters.ts │ │ │ │ ├── CommandExecutor.node.ts │ │ │ │ ├── AssociateSchemaCommand.ts │ │ │ │ └── RemoveSchemaCommand.ts │ │ │ └── KsonSettings.ts │ │ ├── index.browser.ts │ │ └── index.node.ts │ ├── tsconfig.json │ ├── build.gradle.kts │ ├── README.md │ └── package.json ├── cli │ ├── src │ │ └── main │ │ │ └── kotlin │ │ │ └── org │ │ │ └── kson │ │ │ └── tooling │ │ │ └── cli │ │ │ ├── generated │ │ │ └── Version.kt │ │ │ └── CommandLineInterface.kt │ ├── gradle.properties │ ├── pixi.toml │ ├── readme.md │ └── pixi.lock └── readme.md ├── lib-rust ├── .gitignore ├── readme.md ├── .gitattributes ├── kson-sys │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── kson │ ├── Cargo.toml │ ├── src │ │ └── lib.rs │ └── readme.md ├── build.gradle.kts ├── pixiw.bat └── pixi.toml ├── assets └── logo │ ├── kson_logo_blue.png │ ├── kson_logo_transparent.png │ ├── kson_logo_blue_800x800.png │ ├── kson_logo_transparent_800x800.png │ ├── readme.md │ └── generate.sh ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── kson-lib ├── pixi.toml ├── readme.md ├── pixiw.bat └── pixi.lock ├── settings.gradle.kts ├── .gitignore ├── kson-tooling-lib ├── build.gradle.kts └── readme.md ├── gradle.properties └── jdk.properties /lib-python/.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/readme.md: -------------------------------------------------------------------------------- 1 | js-specific code -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib-rust/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | kotlin 3 | -------------------------------------------------------------------------------- /src/jsTest/kotlin/readme.md: -------------------------------------------------------------------------------- 1 | js-specific tests -------------------------------------------------------------------------------- /src/jvmMain/kotlin/readme.md: -------------------------------------------------------------------------------- 1 | jvm-specific code -------------------------------------------------------------------------------- /src/jvmTest/kotlin/readme.md: -------------------------------------------------------------------------------- 1 | jvm-specific tests -------------------------------------------------------------------------------- /lib-rust/readme.md: -------------------------------------------------------------------------------- 1 | See readme at `kson/readme.md` 2 | -------------------------------------------------------------------------------- /src/jsMain/resources/readme.md: -------------------------------------------------------------------------------- 1 | js-specific resources -------------------------------------------------------------------------------- /src/jvmMain/resources/readme.md: -------------------------------------------------------------------------------- 1 | jvm-specific resources -------------------------------------------------------------------------------- /src/nativeKsonTest/readme.md: -------------------------------------------------------------------------------- 1 | native-specific tests -------------------------------------------------------------------------------- /src/nativeMain/kotlin/readme.md: -------------------------------------------------------------------------------- 1 | native-specific code -------------------------------------------------------------------------------- /src/commonMain/resources/readme.md: -------------------------------------------------------------------------------- 1 | all-platform resources -------------------------------------------------------------------------------- /src/jsTest/resources/readme.md: -------------------------------------------------------------------------------- 1 | js-specific test resources -------------------------------------------------------------------------------- /src/jvmTest/resources/readme.md: -------------------------------------------------------------------------------- 1 | jvm-specific test resources -------------------------------------------------------------------------------- /lib-python/setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | py_limited_api = cp310 -------------------------------------------------------------------------------- /src/commonTest/resources/readme.md: -------------------------------------------------------------------------------- 1 | all-platform test resources -------------------------------------------------------------------------------- /src/nativeMain/resources/readme.md: -------------------------------------------------------------------------------- 1 | native-specific resources -------------------------------------------------------------------------------- /src/nativeKsonTest/resources/readme.md: -------------------------------------------------------------------------------- 1 | native-specific test resources -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/BlankFile.kson: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/Number.kson: -------------------------------------------------------------------------------- 1 | 42.42 -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/UnopenedList.kson: -------------------------------------------------------------------------------- 1 | ] -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/NumberError.kson: -------------------------------------------------------------------------------- 1 | 42eope42 -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/Object.kson: -------------------------------------------------------------------------------- 1 | key: value -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/UnclosedString.kson: -------------------------------------------------------------------------------- 1 | "unclosed -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/.gitignore: -------------------------------------------------------------------------------- 1 | .vite 2 | playwright-report 3 | test-results -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/src/connection/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browserConnection'; -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/NumberDanglingExponentError.kson: -------------------------------------------------------------------------------- 1 | key: 42e -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/BlankFile.txt: -------------------------------------------------------------------------------- 1 | FILE(0,0) 2 | -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config'; 2 | export * from './connection'; -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/EmptyCommaError.kson: -------------------------------------------------------------------------------- 1 | [,,,, x ,, y ,,,,,,, z ,,,,] -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/workspace/.gitignore: -------------------------------------------------------------------------------- 1 | # ignore our test .kson files 2 | *.kson 3 | .vscode -------------------------------------------------------------------------------- /assets/logo/kson_logo_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/assets/logo/kson_logo_blue.png -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/rename/foo.xml: -------------------------------------------------------------------------------- 1 | 2 | 1>Foo 3 | 4 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/rename/foo_after.xml: -------------------------------------------------------------------------------- 1 | 2 | Foo 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/UnclosedEmbedBlock.kson: -------------------------------------------------------------------------------- 1 | % 2 | This embed block is not closed... -------------------------------------------------------------------------------- /assets/logo/kson_logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/assets/logo/kson_logo_transparent.png -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/TwoConsecutiveStrings.kson: -------------------------------------------------------------------------------- 1 | "a string" 2 | "an illegal second string" -------------------------------------------------------------------------------- /assets/logo/kson_logo_blue_800x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/assets/logo/kson_logo_blue_800x800.png -------------------------------------------------------------------------------- /assets/logo/kson_logo_transparent_800x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/assets/logo/kson_logo_transparent_800x800.png -------------------------------------------------------------------------------- /lib-rust/.gitattributes: -------------------------------------------------------------------------------- 1 | # SCM syntax highlighting & preventing 3-way merges 2 | pixi.lock merge=binary linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/folding/singleLineShouldNotFold.kson: -------------------------------------------------------------------------------- 1 | brace_list: ["item1", "item2"] 2 | dash_list: - 1 3 | object: {key: "value"} -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/src/worker/ksonServer.ts: -------------------------------------------------------------------------------- 1 | import { createAndStartBrowserWorker } from "@kson/lsp-shared"; 2 | 3 | createAndStartBrowserWorker(); -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/resources/icons/kson_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/tooling/jetbrains/src/main/resources/icons/kson_icon.png -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/extension/assets/kson-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/tooling/lsp-clients/shared/extension/assets/kson-icon.png -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/resources/icons/kson_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kson-org/kson/HEAD/tooling/jetbrains/src/main/resources/icons/kson_icon@2x.png -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/src/server/browser/ksonServerMain.ts: -------------------------------------------------------------------------------- 1 | import { createAndStartBrowserWorker } from "@kson/lsp-shared"; 2 | 3 | createAndStartBrowserWorker(); -------------------------------------------------------------------------------- /tooling/language-server-protocol/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "spec": "src/test/**/*.test.ts", 4 | "node-option": [ 5 | "import=tsx" 6 | ] 7 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/folding/objectFolding.kson: -------------------------------------------------------------------------------- 1 | object: { 2 | key1: "value1", 3 | key2: "value2" 4 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | # Directories to ignore 2 | # Generated for vscode test instance 3 | .vscode-test 4 | .vscode-test-web 5 | # Generated by the es-build 6 | dist -------------------------------------------------------------------------------- /lib-python/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .pytest_cache 3 | .venv 4 | kson-sdist 5 | *.dll 6 | *.dylib 7 | *.so 8 | *.h 9 | *.egg-info 10 | *.pyd 11 | 12 | # uvw generated files 13 | .uv -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/DelimitedDashList.kson: -------------------------------------------------------------------------------- 1 | < 2 | - "one" 3 | - "two" 4 | - < 5 | - "sublist one" 6 | - "sublist two" 7 | > 8 | - "three" 9 | > -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/Number.txt: -------------------------------------------------------------------------------- 1 | FILE(0,5) 2 | KsonPsiElement(IElementTokenType(tokenType=NUMBER))(0,5) 3 | PsiElement(IElementTokenType(tokenType=NUMBER))('42.42')(0,5) -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "outDir": "./dist" 6 | }, 7 | "include": ["src/**/*"] 8 | } -------------------------------------------------------------------------------- /lib-rust/kson-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(dead_code)] 5 | 6 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 7 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/NumberError.txt: -------------------------------------------------------------------------------- 1 | FILE(0,8) 2 | PsiErrorElement:Invalid character `o` found in this number(0,8) 3 | PsiElement(IElementTokenType(tokenType=NUMBER))('42eope42')(0,8) -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true 5 | }, 6 | "include": ["src/**/*", "test/**/*", "test/test-files.json"] 7 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/testSupport/YamlValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.testSupport 2 | 3 | /** 4 | * Validate whether the given [yamlString] parses as legal Yaml 5 | */ 6 | expect fun validateYaml(yamlString: String) -------------------------------------------------------------------------------- /src/jsTest/kotlin/org/kson/testSupport/YamlValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.testSupport 2 | 3 | actual fun validateYaml(yamlString: String) { 4 | // No-op implementation for JS, we verify our test Yaml in the JVM impl 5 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/UnopenedList.txt: -------------------------------------------------------------------------------- 1 | FILE(0,1) 2 | PsiErrorElement:This must close a list, but this is not a list(0,1) 3 | PsiElement(IElementTokenType(tokenType=SQUARE_BRACKET_R))(']')(0,1) -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/readme.md: -------------------------------------------------------------------------------- 1 | # Monaco Editor 2 | 3 | Implementation of a monaco editor using the KSON language client. This LSP is used for the examples and playground on 4 | the [KSON website](https://kson.org). 5 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/KsonLanguage.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains 2 | 3 | import com.intellij.lang.Language 4 | 5 | object KsonLanguage : Language("kson") { 6 | const val NAME = "KSON" 7 | } 8 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/examples/.kson-schema.kson: -------------------------------------------------------------------------------- 1 | { 2 | "schemas": [ 3 | { 4 | "fileMatch": ["config/*.kson", "**/*.config.kson"], 5 | "schema": "schemas/config.schema.json" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/nativeKsonTest/kotlin/org/kson/testSupport/YamlValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.testSupport 2 | 3 | actual fun validateYaml(yamlString: String) { 4 | // No-op implementation for Native, we verify our test Yaml in the JVM impl 5 | } -------------------------------------------------------------------------------- /tooling/language-server-protocol/examples/config/app.config.kson: -------------------------------------------------------------------------------- 1 | appName: "MyApplication" 2 | version: "1.0.0" 3 | server: { 4 | host: "localhost" 5 | port: 8080 6 | enabled: true 7 | } 8 | features: [ 9 | "logging" 10 | "metrics" 11 | ] 12 | -------------------------------------------------------------------------------- /lib-python/src/kson/_marker.c: -------------------------------------------------------------------------------- 1 | // Minimal C extension to mark wheel as platform-specific 2 | #include 3 | static struct PyModuleDef m = {PyModuleDef_HEAD_INIT, "_marker", "", -1, NULL}; 4 | PyMODINIT_FUNC PyInit__marker(void) {return PyModule_Create(&m);} -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/validation/Validator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.validation 2 | 3 | import org.kson.parser.MessageSink 4 | import org.kson.value.KsonValue 5 | 6 | interface Validator { 7 | fun validate(ksonValue: KsonValue, messageSink: MessageSink) 8 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/annotator/stringValues.kson: -------------------------------------------------------------------------------- 1 | { 2 | list: ["not a key", "also not a key"] 3 | value: "this is a value, not a key" 4 | } -------------------------------------------------------------------------------- /tooling/cli/src/main/kotlin/org/kson/tooling/cli/generated/Version.kt: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED FILE - DO NOT EDIT 2 | // Generated by generateVersion task in build.gradle.kts 3 | package org.kson.tooling.cli.generated 4 | 5 | internal const val KSON_VERSION = "0.3.0-SNAPSHOT" -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/psi/KsonPsiElement.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.psi 2 | 3 | import com.intellij.extapi.psi.ASTWrapperPsiElement 4 | import com.intellij.lang.ASTNode 5 | 6 | open class KsonPsiElement(node: ASTNode) : ASTWrapperPsiElement(node) -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/folding/dashListFolding.kson: -------------------------------------------------------------------------------- 1 | simple_list: - "item1" 2 | - "item2" 3 | 4 | nested_list: - "outer1" 5 | - [ 6 | "inner1", 7 | "inner2" 8 | ] -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/test-files.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests": [ 3 | "syntax-highlighting.test", 4 | "status-bar-schema-association.test", 5 | "schema-loading.test", 6 | "formatting-settings.test", 7 | "editing.test", 8 | "diagnostics.test" 9 | ] 10 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/KsonIcons.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | class KsonIcons { 6 | companion object { 7 | val FILE = IconLoader.getIcon("/icons/kson_icon.png", KsonIcons::class.java) 8 | } 9 | } -------------------------------------------------------------------------------- /tooling/cli/gradle.properties: -------------------------------------------------------------------------------- 1 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 2 | org.gradle.configuration-cache = true 3 | 4 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 5 | org.gradle.caching = true 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /tooling/lsp-clients/gradle.properties: -------------------------------------------------------------------------------- 1 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 2 | org.gradle.configuration-cache = true 3 | 4 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 5 | org.gradle.caching = true 6 | -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createKsonEditor } from './KsonEditorManager.js'; 2 | export { createYamlEditor } from './YamlEditorManager.js'; 3 | // Re-export useful types from monaco-editor-wrapper 4 | export type { WrapperConfig, MonacoEditorLanguageClientWrapper } from 'monaco-editor-wrapper'; -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/stdlibx/readme.md: -------------------------------------------------------------------------------- 1 | # Stdlibx: Language/library-level extensions and utilities 2 | 3 | A collection of generally reusable [`stdlib`](https://kotlinlang.org/api/core/kotlin-stdlib/)-type code. 4 | 5 | > Proven elements of this package may graduate to a shared project/library at some point. -------------------------------------------------------------------------------- /tooling/language-server-protocol/gradle.properties: -------------------------------------------------------------------------------- 1 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 2 | org.gradle.configuration-cache = true 3 | 4 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 5 | org.gradle.caching = true 6 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/folding/bracketListFolding.kson: -------------------------------------------------------------------------------- 1 | simple_list: [ 2 | "item1", 3 | "item2", 4 | "item3" 5 | ] 6 | 7 | nested_list: [ 8 | [ 9 | "inner" 10 | ], 11 | "outer" 12 | ] -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/folding/angleListFolding.kson: -------------------------------------------------------------------------------- 1 | simple_list: < 2 | - "item1" 3 | - "item2" 4 | > 5 | 6 | nested_list: < 7 | - "item1" 8 | - < 9 | - "inner" 10 | > 11 | - "item3" 12 | > -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/UnclosedString.txt: -------------------------------------------------------------------------------- 1 | FILE(0,9) 2 | KsonPsiElement([Kson-parsed] QUOTED_STRING)(0,9) 3 | PsiErrorElement:Unclosed string(0,9) 4 | PsiElement(IElementTokenType(tokenType=STRING_OPEN_QUOTE))('"')(0,1) 5 | PsiElement(IElementTokenType(tokenType=STRING_CONTENT))('unclosed')(1,9) 6 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/ManyConstructs.kson: -------------------------------------------------------------------------------- 1 | { 2 | keyword_for_bool: false 3 | "keyword with spaces": "my string value" 4 | list_element: [1,3,3] 5 | my_embed: %sql 6 | this is some rad arbitrary embedded stuff, y'all 7 | %% 8 | sub_object: { 9 | thing: other 10 | } 11 | } -------------------------------------------------------------------------------- /assets/logo/readme.md: -------------------------------------------------------------------------------- 1 | The base logo is stored in [kson_logo_transparent.svg](kson_logo_transparent.svg). After modifying, the [generate.sh](generate.sh) script can be used to regenerate the svg with the blue (`#90bcfe`) background as well as the 400x400 and 800x800 png equivalents. 2 | 3 | This bash script expects `inkscape` to be available on your path. 4 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/org/kson/testSupport/YamlValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.testSupport 2 | 3 | import org.yaml.snakeyaml.Yaml 4 | 5 | actual fun validateYaml(yamlString: String) { 6 | try { 7 | Yaml().load(yamlString) 8 | } catch (e: Exception) { 9 | throw IllegalArgumentException("Invalid YAML: ${e.message}", e) 10 | } 11 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/parser/behavior/readme.md: -------------------------------------------------------------------------------- 1 | # Kson Behavior Documentation 2 | 3 | This package centralizes the "behaviors" for Kson Values, centralizing and owning the "truth" about how they should act. 4 | For instance, in [embedblock](org/kson/parser/behavior/embedblock) we store classes for EmbedBlock specific behaviors 5 | like how they handle indentating -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/stdlibx/exceptions/ShouldNotHappenException.kt: -------------------------------------------------------------------------------- 1 | package org.kson.stdlibx.exceptions 2 | 3 | /** 4 | * An exception for code paths that should never be hit 5 | * 6 | * @param whyNot the reason why we believe that this code path should never be hit 7 | */ 8 | class ShouldNotHappenException(whyNot: String) : RuntimeException("This should not happen:\n$whyNot") -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/annotator/objectKeys.kson: -------------------------------------------------------------------------------- 1 | { 2 | simpleKey: "value" 3 | "quotedKey": "another value" 4 | nested: { 5 | innerKey: true 6 | } 7 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/browser/index.browser.ts: -------------------------------------------------------------------------------- 1 | // Browser test runner entry point 2 | import 'mocha/mocha'; 3 | import { runTests } from '../index.common'; 4 | import { browserConfig } from '../mocha-config'; 5 | 6 | export function run(): Promise { 7 | // Configure mocha for browser 8 | mocha.setup(browserConfig); 9 | 10 | return runTests(mocha, true); 11 | } -------------------------------------------------------------------------------- /kson-lib/pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | authors = ["Adolfo Ochagavía "] 3 | channels = ["conda-forge"] 4 | name = "kson" 5 | platforms = [ 6 | "linux-64", 7 | "linux-aarch64", 8 | "osx-arm64", 9 | "osx-64", 10 | "win-64", 11 | ] 12 | version = "0.1.0" 13 | 14 | [tasks] 15 | 16 | [target.win-64.dependencies] 17 | vs2022_win-64 = ">=19.44.35207,<20" 18 | -------------------------------------------------------------------------------- /kson-lib/readme.md: -------------------------------------------------------------------------------- 1 | # The public interface for Kotlin 2 | 3 | This subproject defines [Kson.kt](src/commonMain/kotlin/org/kson/Kson.kt) the public Kotlin Multiplatform interface for Kson 4 | 5 | TODO: 6 | - confirm we're exposing exactly what we think we are, and add some validations to the build that prevent us from changing that by accident 7 | - define/document how we publish this artifact out 8 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/node/index.node.ts: -------------------------------------------------------------------------------- 1 | // Node.js test runner entry point 2 | import Mocha from 'mocha'; 3 | import { runTests } from '../index.common'; 4 | import { nodeConfig } from '../mocha-config'; 5 | 6 | export function run(): Promise { 7 | // Create the mocha test instance for Node 8 | const mocha = new Mocha(nodeConfig); 9 | 10 | return runTests(mocha, false); 11 | } -------------------------------------------------------------------------------- /lib-rust/kson-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kson-sys" 3 | # [[kson-version-num]] 4 | version = "0.3.0-dev" 5 | edition = "2024" 6 | links = "kson" 7 | description = "Rust bindings to kson-lib" 8 | license = "Apache-2.0" 9 | 10 | [features] 11 | default = [] 12 | 13 | [build-dependencies] 14 | anyhow = "1.0.100" 15 | bindgen = "0.71.0" 16 | flate2 = "1.1.2" 17 | tar = "0.4" 18 | ureq = "3.1.2" 19 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | authors = ["Adolfo Ochagavía "] 3 | channels = ["conda-forge"] 4 | name = "language-server-protocol" 5 | platforms = [ 6 | "linux-64", 7 | "linux-aarch64", 8 | "osx-arm64", 9 | "osx-64", 10 | "win-64", 11 | ] 12 | version = "0.1.0" 13 | 14 | [tasks] 15 | 16 | [dependencies] 17 | nodejs = "20.19.4.*" 18 | -------------------------------------------------------------------------------- /lib-rust/kson/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kson-rs" 3 | # [[kson-version-num]] 4 | version = "0.3.0-dev" 5 | edition = "2024" 6 | description = "Idiomatic Rust bindings to kson-lib" 7 | license = "Apache-2.0" 8 | 9 | [features] 10 | default = [] 11 | 12 | [dependencies] 13 | # [[kson-version-num]] 14 | kson-sys = { version = "0.3.0-dev", path = "../kson-sys" } 15 | 16 | [dev-dependencies] 17 | insta = "1.43.1" 18 | -------------------------------------------------------------------------------- /tooling/cli/pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | authors = ["Adolfo Ochagavía "] 3 | channels = ["conda-forge"] 4 | name = "cli" 5 | platforms = [ 6 | "linux-64", 7 | "linux-aarch64", 8 | "osx-arm64", 9 | "osx-64", 10 | "win-64", 11 | ] 12 | version = "0.1.0" 13 | 14 | [tasks] 15 | 16 | [dependencies] 17 | 18 | [target.win-64.dependencies] 19 | vs2022_win-64 = ">=19.44.35207,<20" 20 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/src/client/common/deactivate.ts: -------------------------------------------------------------------------------- 1 | import {BaseLanguageClient} from "vscode-languageclient"; 2 | 3 | export let languageClient: BaseLanguageClient; 4 | 5 | /** 6 | * Deactivation function for language client. 7 | */ 8 | export async function deactivate(): Promise { 9 | if (languageClient) { 10 | await languageClient.stop(); 11 | languageClient = undefined; 12 | } 13 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/stdlibx/exceptions/FatalParseException.kt: -------------------------------------------------------------------------------- 1 | package org.kson.stdlibx.exceptions 2 | 3 | /** 4 | * An exception for code paths that are fatal if hit during parsing. 5 | * 6 | * These exceptions are caught and result in a null-AST 7 | * 8 | * @param message the reason why we believe that it is unexpected to hit this code path 9 | */ 10 | class FatalParseException(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/editor/KsonCommenterTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | class KsonCommenterTest : KsonEditorActionTest() { 4 | /** 5 | * Sanity check that [KsonCommenter] is correctly hooked up 6 | */ 7 | fun testInsertLineComment() { 8 | doLineCommentTest( 9 | "key: value", 10 | "#key: value" 11 | ) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "compile", 6 | "type": "npm", 7 | "script": "compile-tests", 8 | "group": { 9 | "kind": "build" 10 | }, 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "problemMatcher": ["$tsc"] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/testSupport/JsonValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.testSupport 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | /** 6 | * Validate whether the given [jsonString] parses as legal JSON 7 | */ 8 | fun validateJson(jsonString: String) { 9 | try { 10 | Json.parseToJsonElement(jsonString) 11 | } catch (e: Exception) { 12 | throw IllegalArgumentException("Invalid JSON: ${e.message}", e) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/folding/embedBlockFolding.kson: -------------------------------------------------------------------------------- 1 | dollar_block: $ 2 | function test() { 3 | console.log("Hello"); 4 | } 5 | $$ 6 | 7 | percent_block: % 8 | function test() { 9 | console.log("Hello"); 10 | } 11 | %% 12 | 13 | tagged_block: %kotlin 14 | function test() { 15 | console.log("Hello"); 16 | } 17 | %% -------------------------------------------------------------------------------- /lib-python/setup.py: -------------------------------------------------------------------------------- 1 | # Minimal setup.py - tells setuptools this package has compiled code 2 | from setuptools import setup, Extension 3 | 4 | setup( 5 | ext_modules=[ 6 | Extension( 7 | "kson._marker", 8 | ["src/kson/_marker.c"], 9 | py_limited_api=True, # Use stable ABI for Python 3.x compatibility 10 | define_macros=[("Py_LIMITED_API", "0x030A0000")], # Python 3.10+ stable ABI 11 | ) 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /tooling/lsp-clients/pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | authors = ["Adolfo Ochagavía "] 3 | channels = ["conda-forge"] 4 | name = "language-server-protocol" 5 | platforms = [ 6 | "linux-64", 7 | "linux-aarch64", 8 | "osx-arm64", 9 | "osx-64", 10 | "win-64", 11 | ] 12 | version = "0.1.0" 13 | 14 | [tasks] 15 | 16 | [dependencies] 17 | nodejs = "20.19.4.*" 18 | python = "3.13.*" 19 | 20 | [target.win-64.dependencies] 21 | vc14_runtime = "14.44.*" 22 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/schema/JsonSchemaTestNumber.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema 2 | 3 | import kotlin.test.Test 4 | 5 | class JsonSchemaTestNumber : JsonSchemaTest { 6 | @Test 7 | fun testNumberSchemaWithMinExclusive() { 8 | 9 | assertKsonEnforcesSchema( 10 | """ 11 | 10 12 | """, 13 | """ 14 | {"type": "number", "exclusiveMinimum": 20} 15 | """, 16 | false) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/readme.md: -------------------------------------------------------------------------------- 1 | # KSON VS Code Extension 2 | 3 | This extension provides language support for [KSON](https://kson.org) files in Visual Studio Code. 4 | 5 | ## Features 6 | 7 | - Syntax highlighting for `.kson` files 8 | - Language server integration 9 | - Auto-formatting 10 | - Diagnostics and error reporting 11 | 12 | For issues, feature requests, and source code, please visit the [plugin project home](https://github.com/kson-org/kson/tree/main/tooling/lsp-clients/vscode) -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/src/server/node/ksonServerMain.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from 'vscode-languageserver/node.js'; 2 | import { startKsonServer } from 'kson-language-server/node'; 3 | 4 | // Create connection for Node.js environment 5 | const connection = createConnection(); 6 | 7 | // Set up console logging to use the connection 8 | console.log = connection.console.log.bind(connection.console); 9 | console.error = connection.console.error.bind(connection.console); 10 | 11 | startKsonServer(connection); -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/config/KsonCodeStyleSettings.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.config 2 | 3 | import com.intellij.psi.codeStyle.CodeStyleSettings 4 | import com.intellij.psi.codeStyle.CustomCodeStyleSettings 5 | 6 | class KsonCodeStyleSettings(codeStyleSettings: CodeStyleSettings) 7 | : CustomCodeStyleSettings("KsonCodeStyleSettings", codeStyleSettings) { 8 | 9 | companion object { 10 | const val INDENT_SIZE = 2 11 | const val TAB_SIZE = 2 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/UnclosedEmbedBlock.txt: -------------------------------------------------------------------------------- 1 | FILE(0,35) 2 | PsiErrorElement:Unclosed "%"(0,35) 3 | KsonPsiElement(IElementTokenType(tokenType=EMBED_OPEN_DELIM))(0,1) 4 | PsiElement(IElementTokenType(tokenType=EMBED_OPEN_DELIM))('%')(0,1) 5 | PsiElement(IElementTokenType(tokenType=EMBED_PREAMBLE_NEWLINE))('\n')(1,2) 6 | KsonEmbedContent(IElementTokenType(tokenType=EMBED_CONTENT))(2,35) 7 | PsiElement(IElementTokenType(tokenType=EMBED_CONTENT))('This embed block is not closed...')(2,35) 8 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/psi/KsonPsiFile.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.psi 2 | 3 | import com.intellij.extapi.psi.PsiFileBase 4 | import com.intellij.openapi.fileTypes.FileType 5 | import com.intellij.psi.FileViewProvider 6 | import org.kson.jetbrains.KsonLanguage 7 | import org.kson.jetbrains.file.KsonFileType 8 | 9 | class KsonPsiFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, KsonLanguage) { 10 | override fun getFileType(): FileType { 11 | return KsonFileType 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--disable-extensions", 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/**/*.js" 14 | ], 15 | "preLaunchTask": "compile", 16 | "sourceMaps": true, 17 | "trace": true, 18 | "env": { 19 | "KSON_DEBUG_PORT": "6009" 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/mocha-config.ts: -------------------------------------------------------------------------------- 1 | // Centralized Mocha configuration 2 | export interface MochaConfig { 3 | ui: 'bdd' | 'tdd' | 'qunit' | 'exports'; 4 | timeout: number; 5 | color?: boolean; 6 | reporter?: string; 7 | } 8 | 9 | export const commonConfig: MochaConfig = { 10 | ui: 'bdd', 11 | timeout: 10000, 12 | }; 13 | 14 | export const nodeConfig: MochaConfig = { 15 | ...commonConfig, 16 | color: true, 17 | }; 18 | 19 | export const browserConfig: MochaConfig = { 20 | ...commonConfig, 21 | reporter: undefined 22 | }; -------------------------------------------------------------------------------- /lib-rust/kson/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod generated; 2 | #[cfg(test)] 3 | mod test; 4 | 5 | pub use generated::*; 6 | 7 | fn kson_result_into_rust_result(r: Result) -> std::result::Result { 8 | match r { 9 | Result::Success(s) => Ok(s), 10 | Result::Failure(f) => Err(f), 11 | } 12 | } 13 | 14 | fn kson_schema_result_into_rust_result(r: SchemaResult) -> std::result::Result { 15 | match r { 16 | SchemaResult::Success(s) => Ok(s), 17 | SchemaResult::Failure(f) => Err(f), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/highlighter/KsonSyntaxHighlighterFactory.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.highlighter 2 | 3 | import com.intellij.openapi.fileTypes.SyntaxHighlighter 4 | import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.vfs.VirtualFile 7 | 8 | 9 | class KsonSyntaxHighlighterFactory : SyntaxHighlighterFactory() { 10 | override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?): SyntaxHighlighter { 11 | return KsonSyntaxHighlighter() 12 | } 13 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | plugins { 3 | kotlin("multiplatform") version "2.2.20" 4 | kotlin("jvm") version "2.2.20" 5 | kotlin("plugin.serialization") version "2.2.20" 6 | } 7 | 8 | repositories { 9 | gradlePluginPortal() 10 | mavenCentral() 11 | } 12 | } 13 | 14 | rootProject.name = "kson" 15 | include("kson-lib") 16 | include("kson-tooling-lib") 17 | include("lib-python") 18 | include("lib-rust") 19 | include("tooling:jetbrains") 20 | include("tooling:language-server-protocol") 21 | include("tooling:lsp-clients") 22 | include("tooling:cli") 23 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/Object.txt: -------------------------------------------------------------------------------- 1 | FILE(0,10) 2 | KsonPsiElement([Kson-parsed] OBJECT)(0,10) 3 | KsonPsiElement([Kson-parsed] OBJECT_PROPERTY)(0,10) 4 | KsonPsiElement([Kson-parsed] OBJECT_KEY)(0,3) 5 | KsonPsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))(0,3) 6 | PsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))('key')(0,3) 7 | PsiElement(IElementTokenType(tokenType=COLON))(':')(3,4) 8 | KsonPsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))(5,10) 9 | PsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))('value')(5,10) 10 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/AllOfValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonValue 4 | import org.kson.parser.MessageSink 5 | import org.kson.schema.JsonSchema 6 | import org.kson.schema.JsonSchemaValidator 7 | 8 | class AllOfValidator(val allOf: List) : JsonSchemaValidator { 9 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 10 | // log any and all error we see from this collection of schemas we must satisfy 11 | allOf.forEach { 12 | it.validate(ksonValue, messageSink) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MaxItemsValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonList 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonArrayValidator 7 | 8 | class MaxItemsValidator(private val maxItems: Long) : JsonArrayValidator() { 9 | override fun validateArray(node: KsonList, messageSink: MessageSink) { 10 | if (node.elements.size > maxItems) { 11 | messageSink.error(node.location, MessageType.SCHEMA_ARRAY_TOO_LONG.create(maxItems.toString())) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MinItemsValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonList 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonArrayValidator 7 | 8 | class MinItemsValidator(private val minItems: Long) : JsonArrayValidator() { 9 | override fun validateArray(node: KsonList, messageSink: MessageSink) { 10 | if (node.elements.size < minItems) { 11 | messageSink.error(node.location, MessageType.SCHEMA_ARRAY_TOO_SHORT.create(minItems.toString())) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/PropertyNamesValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonObject 4 | import org.kson.parser.MessageSink 5 | import org.kson.schema.JsonObjectValidator 6 | import org.kson.schema.JsonSchema 7 | 8 | class PropertyNamesValidator(private val propertyNamesSchema: JsonSchema?) : JsonObjectValidator() { 9 | override fun validateObject(node: KsonObject, messageSink: MessageSink) { 10 | node.propertyMap.forEach { (_, property) -> 11 | propertyNamesSchema?.validate(property.propName, messageSink) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/KsonCoreTestNumberError.kt: -------------------------------------------------------------------------------- 1 | package org.kson 2 | 3 | import org.kson.parser.messages.MessageType.* 4 | import kotlin.test.Test 5 | 6 | class KsonCoreTestNumberError : KsonCoreTestError { 7 | @Test 8 | fun testDanglingExponentError() { 9 | assertParserRejectsSource( 10 | """ 11 | 420E 12 | """, 13 | listOf(DANGLING_EXP_INDICATOR) 14 | ) 15 | 16 | assertParserRejectsSource( 17 | """ 18 | 420E- 19 | """, 20 | listOf(DANGLING_EXP_INDICATOR) 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/NumberDanglingExponentError.txt: -------------------------------------------------------------------------------- 1 | FILE(0,8) 2 | KsonPsiElement([Kson-parsed] OBJECT)(0,8) 3 | KsonPsiElement([Kson-parsed] OBJECT_PROPERTY)(0,8) 4 | KsonPsiElement([Kson-parsed] OBJECT_KEY)(0,3) 5 | KsonPsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))(0,3) 6 | PsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))('key')(0,3) 7 | PsiElement(IElementTokenType(tokenType=COLON))(':')(3,4) 8 | PsiErrorElement:Dangling exponent error: `e` must be followed by an exponent(5,8) 9 | PsiElement(IElementTokenType(tokenType=NUMBER))('42e')(5,8) 10 | -------------------------------------------------------------------------------- /tooling/cli/readme.md: -------------------------------------------------------------------------------- 1 | # KSON CLI 2 | 3 | A command-line interface for working with KSON files. 4 | 5 | ## Building 6 | 7 | To build a native executable, run: 8 | 9 | ```bash 10 | ./gradlew :tooling:cli:buildNativeImage 11 | ``` 12 | 13 | The native binary will be generated in `tooling/cli/build/native/nativeCompile/`. 14 | 15 | ## Usage 16 | 17 | The CLI provides several commands for working with KSON files: 18 | 19 | - `format` - Format KSON files 20 | - `analyze` - Analyze KSON files for issues 21 | - `json` or `yaml` - Convert KSON files to other formats 22 | 23 | Run the CLI with `--help` for more information on available commands and options. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Always-ignore extensions for common app-generated files 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | *.iml 13 | *.md.html 14 | 15 | # OS or Editor folders 16 | .DS_Store 17 | .cache 18 | .project 19 | .settings 20 | .tmproj 21 | nbproject 22 | Thumbs.db 23 | 24 | ## Directories to ignore 25 | # build directories 26 | out 27 | target 28 | build 29 | dist 30 | # gradlew generated files 31 | .gradle 32 | gradle/jdk 33 | # intellij settings 34 | .idea 35 | # kotlin metadata 36 | .kotlin 37 | # node generated files 38 | node_modules 39 | # pixi files 40 | .pixi 41 | pixiw 42 | pixiw.bat -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/schema/createSchemaProvider.browser.ts: -------------------------------------------------------------------------------- 1 | import {SchemaProvider} from './SchemaProvider.js'; 2 | import {URI} from "vscode-uri"; 3 | 4 | /** 5 | * Browser-specific schema provider factory. 6 | * Returns undefined since browsers don't have file system access. 7 | */ 8 | export async function createSchemaProvider( 9 | workspaceRootUri: URI | undefined, 10 | logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void } 11 | ): Promise { 12 | logger.info('Running in browser environment, schema provider disabled'); 13 | return undefined; 14 | } -------------------------------------------------------------------------------- /tooling/readme.md: -------------------------------------------------------------------------------- 1 | # [Kson](https://kson.org) Tooling 2 | 3 | As a love letter to humans maintaining computer configuration, [Kson](https://kson.org) considers tooling integral. This folder contains the sources of official tools that are developed in tandem with the language to serve and delight all those lovely humans 4 | 5 | ### 🛠 💌 ➡ 👥 6 | 7 | ## Contents 8 | 9 | * [Kson support for JetBrains IDEs](jetbrains/readme.md) 10 | * [Implementation of the Language Server Protocol](./language-server-protocol) 11 | * [Monaco Editor](./lsp-clients/monaco) 12 | * [VS Code Plugin](./lsp-clients/vscode) 13 | * [Command Line Tool](./cli) 14 | * More to come... 15 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/features/FormattingService.ts: -------------------------------------------------------------------------------- 1 | import {TextEdit} from 'vscode-languageserver'; 2 | import {KsonDocument} from '../document/KsonDocument.js'; 3 | import {FormatOptions, Kson} from 'kson'; 4 | 5 | /** 6 | * Service responsible for formatting Kson documents 7 | */ 8 | export class FormattingService { 9 | 10 | formatDocument(document: KsonDocument, formatOptions: FormatOptions): TextEdit[] { 11 | const formattedKson = Kson.getInstance().format(document.getText(), formatOptions); 12 | 13 | return [ 14 | TextEdit.replace(document.getFullDocumentRange(), formattedKson) 15 | ]; 16 | } 17 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MaximumValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonNumber 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonNumberValidator 7 | 8 | class MaximumValidator(private val maximum: Double) : JsonNumberValidator() { 9 | override fun validateNumber(node: KsonNumber, messageSink: MessageSink) { 10 | val number = node.value.asDouble 11 | if (number > maximum) { 12 | messageSink.error(node.location, MessageType.SCHEMA_VALUE_TOO_LARGE.create(maximum.toString())) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MinimumValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonNumber 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonNumberValidator 7 | 8 | class MinimumValidator(private val minimum: Double) : JsonNumberValidator() { 9 | override fun validateNumber(node: KsonNumber, messageSink: MessageSink) { 10 | val number = node.value.asDouble 11 | if (number < minimum) { 12 | messageSink.error(node.location, MessageType.SCHEMA_VALUE_TOO_SMALL.create(minimum.toString())) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/CommandExecutorFactory.ts: -------------------------------------------------------------------------------- 1 | import {Connection} from "vscode-languageserver"; 2 | import {FormattingService} from "../features/FormattingService"; 3 | import {KsonDocumentsManager} from "../document/KsonDocumentsManager"; 4 | import {KsonSettings} from "../KsonSettings"; 5 | import {CommandExecutorBase} from "./CommandExecutor.base"; 6 | 7 | export type CommandExecutorFactory = ( 8 | connection: Connection, 9 | documentManager: KsonDocumentsManager, 10 | formattingService: FormattingService, 11 | getConfiguration: () => Required, 12 | workspaceRoot: string | null 13 | ) => CommandExecutorBase; 14 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MaxPropertiesValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonObject 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonObjectValidator 7 | 8 | class MaxPropertiesValidator(private val maxProperties: Long) : JsonObjectValidator() { 9 | override fun validateObject(node: KsonObject, messageSink: MessageSink) { 10 | if (node.propertyMap.size > maxProperties) { 11 | messageSink.error(node.location, MessageType.SCHEMA_OBJECT_TOO_MANY_PROPERTIES.create(maxProperties.toString())) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/highlighter/KsonSemanticHighlightAnnotatorTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.highlighter 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 4 | 5 | class KsonSemanticHighlightAnnotatorTest : BasePlatformTestCase() { 6 | 7 | override fun getTestDataPath(): String { 8 | return "src/test/resources/testData/annotator" 9 | } 10 | 11 | fun testObjectKeys() { 12 | myFixture.testHighlighting(false, true, false, "objectKeys.kson") 13 | } 14 | 15 | fun testStringValues() { 16 | myFixture.testHighlighting(false, true, false, "stringValues.kson") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MaxLengthValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonString 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonStringValidator 7 | 8 | class MaxLengthValidator(private val maxLength: Long) : JsonStringValidator() { 9 | override fun validateString(node: KsonString, messageSink: MessageSink) { 10 | val str = node.value 11 | if (countCodePoints(str) > maxLength) { 12 | messageSink.error(node.location, MessageType.SCHEMA_STRING_LENGTH_TOO_LONG.create(maxLength.toString())) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/src/YamlEditorManager.ts: -------------------------------------------------------------------------------- 1 | import { MonacoEditorLanguageClientWrapper, WrapperConfig } from 'monaco-editor-wrapper'; 2 | import { createYamlConfig } from './config/yamlConfig.js'; 3 | 4 | /** 5 | * Creates a YAML editor with syntax highlighting support. 6 | * Returns the wrapper which can be managed using its native API. 7 | */ 8 | export async function createYamlEditor(config: Partial = {}): Promise { 9 | const yamlConfig = createYamlConfig(config); 10 | 11 | const wrapper = new MonacoEditorLanguageClientWrapper(); 12 | await wrapper.init(yamlConfig); 13 | 14 | return wrapper; 15 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MinLengthValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonString 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonStringValidator 7 | 8 | class MinLengthValidator(private val minLength: Long) : JsonStringValidator() { 9 | override fun validateString(node: KsonString, messageSink: MessageSink) { 10 | val str = node.value 11 | if (countCodePoints(str) < minLength) { 12 | messageSink.error(node.location, MessageType.SCHEMA_STRING_LENGTH_TOO_SHORT.create(minLength.toString())) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/EnumValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonList 4 | import org.kson.value.KsonValue 5 | import org.kson.parser.MessageSink 6 | import org.kson.parser.messages.MessageType 7 | import org.kson.schema.JsonSchemaValidator 8 | 9 | class EnumValidator(private val enum: KsonList) : JsonSchemaValidator { 10 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 11 | val enumValues = enum.elements 12 | if (!enumValues.contains(ksonValue)) { 13 | messageSink.error(ksonValue.location, MessageType.SCHEMA_ENUM_VALUE_NOT_ALLOWED.create()) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/index.browser.ts: -------------------------------------------------------------------------------- 1 | // Browser-specific exports 2 | import { Connection } from 'vscode-languageserver'; 3 | import { startKsonServer as startKsonServerCore } from './startKsonServer.js'; 4 | import { createSchemaProvider } from './core/schema/createSchemaProvider.browser.js'; 5 | import { createCommandExecutor } from './core/commands/createCommandExecutor.browser.js'; 6 | 7 | /** 8 | * Starts the Kson Language Server for browser environments. 9 | * No schema provider or file system-based command support. 10 | */ 11 | export function startKsonServer(connection: Connection): void { 12 | startKsonServerCore(connection, createSchemaProvider, createCommandExecutor); 13 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/PatternValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonString 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonStringValidator 7 | 8 | class PatternValidator(pattern: String) : JsonStringValidator() { 9 | private val pattern = Regex(pattern) 10 | override fun validateString(node: KsonString, messageSink: MessageSink) { 11 | val str = node.value 12 | if (!pattern.containsMatchIn(str)) { 13 | messageSink.error(node.location, MessageType.SCHEMA_STRING_PATTERN_MISMATCH.create(pattern.pattern)) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/index.node.ts: -------------------------------------------------------------------------------- 1 | // Node.js-specific exports 2 | import { Connection } from 'vscode-languageserver'; 3 | import { startKsonServer as startKsonServerCore } from './startKsonServer.js'; 4 | import { createSchemaProvider } from './core/schema/createSchemaProvider.node.js'; 5 | import { createCommandExecutor } from './core/commands/createCommandExecutor.node.js'; 6 | 7 | /** 8 | * Starts the Kson Language Server for Node.js environments. 9 | * Includes file system-based schema provider and command executor support. 10 | */ 11 | export function startKsonServer(connection: Connection): void { 12 | startKsonServerCore(connection, createSchemaProvider, createCommandExecutor); 13 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MinPropertiesValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonObject 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonObjectValidator 7 | 8 | class MinPropertiesValidator(private val minProperties: Long) : JsonObjectValidator() { 9 | override fun validateObject(node: KsonObject, messageSink: MessageSink) { 10 | if (node.propertyMap.size < minProperties) { 11 | messageSink.error(node.location, 12 | MessageType.SCHEMA_OBJECT_TOO_FEW_PROPERTIES.create(minProperties.toString()) 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/parser/json/readme.md: -------------------------------------------------------------------------------- 1 | # JSON Tests 2 | 3 | Kson is a superset of JSON. The tests in this directory validate that compliance 4 | 5 | The tests in [`generated/JsonSuiteTest`](generated/JsonSuiteTest.kt) are based on the [JSONTestSuite](https://github.com/nst/JSONTestSuite) project. They are generated by the `generateJsonTestSuite` task defined in the root [build.gradle.kts](../../../../../../../build.gradle.kts) 6 | > See [GenerateJsonTestSuiteTask](../../../../../../../buildSrc/src/main/kotlin/GenerateJsonTestSuiteTask.kt) and [JsonTestSuiteGenerator](../../../../../../../buildSrc/src/main/kotlin/org/kson/jsonsuite/JsonTestSuiteGenerator.kt) for more details on the `generateJsonTestSuite` task -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/ExclusiveMaximumValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonNumber 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonNumberValidator 7 | 8 | class ExclusiveMaximumValidator(private val exclusiveMaximum: Double) : JsonNumberValidator() { 9 | override fun validateNumber(node: KsonNumber, messageSink: MessageSink) { 10 | val number = node.value.asDouble 11 | if (number >= exclusiveMaximum) { 12 | messageSink.error(node.location, MessageType.SCHEMA_VALUE_TOO_LARGE_EXCLUSIVE.create(exclusiveMaximum.toString())) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/ExclusiveMinimumValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonNumber 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonNumberValidator 7 | 8 | class ExclusiveMinimumValidator(private val exclusiveMinimum: Double) : JsonNumberValidator() { 9 | override fun validateNumber(node: KsonNumber, messageSink: MessageSink) { 10 | val number = node.value.asDouble 11 | if (number <= exclusiveMinimum) { 12 | messageSink.error(node.location, MessageType.SCHEMA_VALUE_TOO_SMALL_EXCLUSIVE.create(exclusiveMinimum.toString())) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/schema/JsonSchemaTestArray.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema 2 | 3 | import kotlin.test.Test 4 | 5 | class JsonSchemaTestArray : JsonSchemaTest { 6 | @Test 7 | fun testEnforceConstraintsForMultipleTypes() { 8 | 9 | assertKsonEnforcesSchema( 10 | """ 11 | [1, "string", true] 12 | """, 13 | """ 14 | {"contains": { "type": "boolean" }} 15 | """, 16 | true) 17 | 18 | assertKsonEnforcesSchema( 19 | """ 20 | [1, "string"] 21 | """, 22 | """ 23 | {"contains": { "type": "boolean" }} 24 | """, 25 | false) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/editor/KsonQuoteHandler.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler 3 | import com.intellij.openapi.editor.highlighter.HighlighterIterator 4 | import org.kson.jetbrains.parser.elem 5 | import org.kson.parser.TokenType 6 | 7 | 8 | class KsonQuoteHandler : SimpleTokenSetQuoteHandler(elem(TokenType.STRING_OPEN_QUOTE), elem(TokenType.STRING_CLOSE_QUOTE)) { 9 | 10 | override fun isOpeningQuote(iterator: HighlighterIterator?, offset: Int): Boolean { 11 | if (iterator == null) { 12 | return false 13 | } 14 | 15 | return iterator.tokenType == elem(TokenType.STRING_OPEN_QUOTE) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tooling/lsp-clients/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "bundler", 6 | "outDir": "out", 7 | // Custom types 8 | "typeRoots": [ 9 | "./node_modules/@types" 10 | ], 11 | // Useful settings for debugging 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | "inlineSourceMap": false, 16 | "inlineSources": true, 17 | // CommonJS interop 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | // Prevents cross-platform issues 21 | "forceConsistentCasingInFileNames": true, 22 | // Performance improvement 23 | "skipLibCheck": true 24 | } 25 | } -------------------------------------------------------------------------------- /lib-rust/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins{ 2 | base 3 | } 4 | 5 | val nativeKsonDir = project.projectDir.parentFile.resolve("kson-lib/build/kotlin/compileGraalVmNativeImage") 6 | val ksonRsTargetDir = project.projectDir.resolve("kson/target/debug") 7 | 8 | val testDynamic by tasks.registering(PixiExecTask::class) { 9 | dependsOn(":kson-lib:buildWithGraalVmNativeImage") 10 | 11 | group="verification" 12 | command=listOf("cargo", "test", "--manifest-path", "kson/Cargo.toml") 13 | envVars=mapOf( 14 | Pair("KSON_PREBUILT_BIN_DIR", nativeKsonDir.absolutePath), 15 | Pair("KSON_COPY_SHARED_LIBRARY_TO_DIR", ksonRsTargetDir.absolutePath), 16 | ) 17 | } 18 | 19 | tasks{ 20 | check { 21 | dependsOn(testDynamic) 22 | } 23 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/KsonBundle.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains 2 | 3 | import com.intellij.DynamicBundle 4 | import org.jetbrains.annotations.NonNls 5 | import org.jetbrains.annotations.PropertyKey 6 | 7 | @NonNls 8 | private const val BUNDLE = "messages.KsonBundle" 9 | 10 | object KsonBundle : DynamicBundle(BUNDLE) { 11 | 12 | @Suppress("SpreadOperator") 13 | @JvmStatic 14 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 15 | getMessage(key, *params) 16 | 17 | @Suppress("SpreadOperator", "unused") 18 | @JvmStatic 19 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 20 | getLazyMessage(key, *params) 21 | } 22 | -------------------------------------------------------------------------------- /kson-lib/pixiw.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Simple Pixi wrapper for Windows that auto-installs Pixi locally if needed 4 | 5 | set PIXI_DIR=%cd%\..\.pixi 6 | set PIXI_BIN=%PIXI_DIR%\bin\pixi.exe 7 | 8 | rem Install Pixi locally if not present 9 | if not exist "%PIXI_BIN%" ( 10 | echo Installing Pixi locally to %PIXI_DIR%... 11 | 12 | rem Use Pixi's official installation script with custom install location 13 | set PIXI_HOME=%PIXI_DIR% 14 | set PIXI_NO_PATH_UPDATE=1 15 | 16 | powershell -ExecutionPolicy ByPass -Command "iwr -useb https://pixi.sh/install.ps1 | iex" 17 | 18 | if not exist "%PIXI_BIN%" ( 19 | echo Failed to install Pixi 20 | exit /b 1 21 | ) 22 | ) 23 | 24 | rem Execute Pixi with all arguments 25 | "%PIXI_BIN%" %* -------------------------------------------------------------------------------- /lib-rust/pixiw.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Simple Pixi wrapper for Windows that auto-installs Pixi locally if needed 4 | 5 | set PIXI_DIR=%cd%\..\.pixi 6 | set PIXI_BIN=%PIXI_DIR%\bin\pixi.exe 7 | 8 | rem Install Pixi locally if not present 9 | if not exist "%PIXI_BIN%" ( 10 | echo Installing Pixi locally to %PIXI_DIR%... 11 | 12 | rem Use Pixi's official installation script with custom install location 13 | set PIXI_HOME=%PIXI_DIR% 14 | set PIXI_NO_PATH_UPDATE=1 15 | 16 | powershell -ExecutionPolicy ByPass -Command "iwr -useb https://pixi.sh/install.ps1 | iex" 17 | 18 | if not exist "%PIXI_BIN%" ( 19 | echo Failed to install Pixi 20 | exit /b 1 21 | ) 22 | ) 23 | 24 | rem Execute Pixi with all arguments 25 | "%PIXI_BIN%" %* -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | testDir: './tests', 5 | timeout: 30 * 1000, 6 | expect: { 7 | timeout: 5000 8 | }, 9 | fullyParallel: true, 10 | forbidOnly: !!process.env.CI, 11 | retries: process.env.CI ? 2 : 0, 12 | workers: process.env.CI ? 1 : undefined, 13 | reporter: 'html', 14 | use: { 15 | baseURL: 'http://localhost:5173', 16 | trace: 'on-first-retry', 17 | }, 18 | 19 | projects: [ 20 | { 21 | name: 'chromium', 22 | use: { ...devices['Desktop Chrome'] }, 23 | }, 24 | ], 25 | 26 | webServer: { 27 | command: 'npm run dev', 28 | port: 5173, 29 | reuseExistingServer: !process.env.CI, 30 | }, 31 | }); -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/NotValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonValue 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonSchema 7 | import org.kson.schema.JsonSchemaValidator 8 | 9 | class NotValidator(private val notSchema: JsonSchema?) : JsonSchemaValidator { 10 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 11 | if (notSchema == null) { 12 | return 13 | } 14 | val notMessageSink = MessageSink() 15 | if (notSchema.isValid(ksonValue, notMessageSink)) { 16 | messageSink.error(ksonValue.location, MessageType.SCHEMA_NOT_VALIDATION_FAILED.create()) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "outDir": "out", 7 | // Custom types 8 | "typeRoots": [ 9 | "./node_modules/@types" 10 | ], 11 | // Useful settings for debugging 12 | "declaration": true, 13 | "declarationMap": true, 14 | "sourceMap": true, 15 | "inlineSourceMap": false, 16 | "inlineSources": true, 17 | // CommonJS interop 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | // Prevents cross-platform issues 21 | "forceConsistentCasingInFileNames": true, 22 | // Performance improvement 23 | "skipLibCheck": true 24 | }, 25 | "include": [ 26 | "src" 27 | ] 28 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/file/KsonFileType.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.file 2 | 3 | import com.intellij.openapi.fileTypes.LanguageFileType 4 | import org.kson.jetbrains.KsonBundle 5 | import org.kson.jetbrains.KsonIcons 6 | import org.kson.jetbrains.KsonLanguage 7 | import javax.swing.Icon 8 | 9 | object KsonFileType : LanguageFileType(KsonLanguage) { 10 | 11 | override fun getName(): String { 12 | return KsonLanguage.NAME 13 | } 14 | 15 | override fun getDescription(): String { 16 | return KsonBundle.message("kson.file.fileTypeDescription") 17 | } 18 | 19 | override fun getDefaultExtension(): String { 20 | return "kson" 21 | } 22 | 23 | override fun getIcon(): Icon { 24 | return KsonIcons.FILE 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/TwoConsecutiveStrings.txt: -------------------------------------------------------------------------------- 1 | FILE(0,37) 2 | KsonPsiElement([Kson-parsed] QUOTED_STRING)(0,10) 3 | PsiElement(IElementTokenType(tokenType=STRING_OPEN_QUOTE))('"')(0,1) 4 | PsiElement(IElementTokenType(tokenType=STRING_CONTENT))('a string')(1,9) 5 | PsiElement(IElementTokenType(tokenType=STRING_CLOSE_QUOTE))('"')(9,10) 6 | PsiErrorElement:Unexpected trailing content. The previous content parsed as a complete Kson document.(11,37) 7 | KsonPsiElement([Kson-parsed] QUOTED_STRING)(11,37) 8 | PsiElement(IElementTokenType(tokenType=STRING_OPEN_QUOTE))('"')(11,12) 9 | PsiElement(IElementTokenType(tokenType=STRING_CONTENT))('an illegal second string')(12,36) 10 | PsiElement(IElementTokenType(tokenType=STRING_CLOSE_QUOTE))('"')(36,37) -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import tmLanguageJson from '../../extension/config/kson.tmLanguage.json'; 2 | import languageConfigJson from '../../extension/config/language-configuration.json'; 3 | 4 | export const tmLanguage = tmLanguageJson; 5 | export const languageConfiguration = languageConfigJson; 6 | 7 | // Export as strings for different consumption methods 8 | export const tmLanguageString = JSON.stringify(tmLanguageJson, null, 2); 9 | export const languageConfigurationString = JSON.stringify(languageConfigJson, null, 2); 10 | 11 | // Export individual config properties for type safety 12 | export const KSON_LANGUAGE_ID = 'kson'; 13 | export const KSON_EXTENSIONS = ['.kson']; 14 | export const KSON_ALIASES = ['Kson', 'kson', 'KSON']; 15 | export const KSON_SCOPE_NAME = 'source.kson'; -------------------------------------------------------------------------------- /lib-python/readme.md: -------------------------------------------------------------------------------- 1 | # Python bindings for Kson's public API 2 | 3 | [KSON](https://kson.org) is available on PyPI for Linux, macOS and Windows. 4 | 5 | ## Installation 6 | 7 | Install from PyPI: 8 | 9 | ```bash 10 | pip install kson-lang 11 | ``` 12 | 13 | ### Build from source 14 | 15 | ```bash 16 | git clone https://github.com/kson-org/kson.git 17 | cd kson && ./gradlew :lib-python:build 18 | pip install ./lib-python 19 | ``` 20 | 21 | ## Example usage 22 | 23 | 24 | ```python 25 | from kson import Kson, Success 26 | result = Kson.to_json("key: [1, 2, 3, 4]") 27 | assert isinstance(result, Success) 28 | print(result.output()) 29 | ``` 30 | 31 | This should print the following to stdout: 32 | 33 | ```json 34 | { 35 | "key": [ 36 | 1, 37 | 2, 38 | 3, 39 | 4 40 | ] 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/resources/messages/KsonBundle.properties: -------------------------------------------------------------------------------- 1 | kson.name=Kson 2 | kson.file.fileTypeDescription=Kson language file 3 | 4 | kson.syntaxHighlighter.curly_brace=Curly Brace 5 | kson.syntaxHighlighter.square_bracket=Square Bracket 6 | kson.syntaxHighlighter.angle_bracket=Angle Bracket 7 | kson.syntaxHighlighter.colon=Colon 8 | kson.syntaxHighlighter.comma=Comma 9 | kson.syntaxHighlighter.comment=Comment 10 | kson.syntaxHighlighter.end-dot=End-dot 11 | kson.syntaxHighlighter.delimiter=Delimiter 12 | kson.syntaxHighlighter.embed_tag=Embed tag 13 | kson.syntaxHighlighter.unquoted=Unquoted String 14 | kson.syntaxHighlighter.keyword=Keyword 15 | kson.syntaxHighlighter.invalid=Invalid 16 | kson.syntaxHighlighter.number=Number 17 | kson.syntaxHighlighter.content=Content 18 | kson.syntaxHighlighter.key=Key 19 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/schema/JsonSchemaTestType.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema 2 | 3 | import kotlin.test.Test 4 | 5 | class JsonSchemaTestType : JsonSchemaTest { 6 | @Test 7 | fun testEnforceConstraintsForMultipleTypes() { 8 | 9 | assertKsonEnforcesSchema( 10 | """ 11 | "this string is longer than 10 characters" 12 | """, 13 | """ 14 | {"type": ["string", "number"], "exclusiveMinimum": 20, "maxLength": 10 } 15 | """, 16 | false) 17 | 18 | assertKsonEnforcesSchema( 19 | """ 20 | 10 21 | """, 22 | """ 23 | {"type": ["string", "number"], "exclusiveMinimum": 20, "maxLength": 10 } 24 | """, 25 | false) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/editor/KsonCommenter.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | import com.intellij.lang.Commenter 4 | 5 | /** 6 | * Configure a [Commenter] for Kson. Note this returns mostly nulls since Kson does not have 7 | * a Block Comment construct 8 | */ 9 | internal class KsonCommenter : Commenter { 10 | override fun getLineCommentPrefix(): String { 11 | return "#" 12 | } 13 | 14 | override fun getBlockCommentPrefix(): String? { 15 | return null 16 | } 17 | 18 | override fun getBlockCommentSuffix(): String? { 19 | return null 20 | } 21 | 22 | override fun getCommentedBlockCommentPrefix(): String? { 23 | return null 24 | } 25 | 26 | override fun getCommentedBlockCommentSuffix(): String? { 27 | return null 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/KsonCoreTestBasicLiteral.kt: -------------------------------------------------------------------------------- 1 | package org.kson 2 | 3 | import kotlin.test.Test 4 | 5 | class KsonCoreTestBasicLiteral : KsonCoreTest { 6 | @Test 7 | fun testBooleanLiteralSource() { 8 | assertParsesTo( 9 | """ 10 | true 11 | """, 12 | "true", 13 | "true", 14 | "true" 15 | ) 16 | 17 | assertParsesTo( 18 | """ 19 | false 20 | """, 21 | "false", 22 | "false", 23 | "false" 24 | ) 25 | } 26 | 27 | @Test 28 | fun testNullLiteralSource() { 29 | assertParsesTo( 30 | """ 31 | null 32 | """, 33 | "null", 34 | "null", 35 | "null" 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kson-tooling-lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | group = "org.kson" 10 | version = "0.1.2-SNAPSHOT" 11 | kotlin { 12 | jvm { 13 | testRuns["test"].executionTask.configure { 14 | useJUnit() 15 | } 16 | } 17 | js(IR) { 18 | browser() 19 | nodejs() 20 | binaries.library() 21 | useEsModules() 22 | generateTypeScriptDefinitions() 23 | } 24 | 25 | sourceSets { 26 | val commonMain by getting { 27 | dependencies { 28 | implementation(project(":")) 29 | } 30 | } 31 | val commonTest by getting { 32 | dependencies { 33 | implementation(kotlin("test")) 34 | } 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /lib-rust/pixi.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | authors = ["Adolfo Ochagavía "] 3 | channels = ["conda-forge"] 4 | name = "lib-rust" 5 | platforms = [ 6 | "linux-64", 7 | "linux-aarch64", 8 | "osx-arm64", 9 | "osx-64", 10 | "win-64", 11 | ] 12 | # [[kson-version-num]] 13 | version = "0.3.0-dev" 14 | 15 | [tasks] 16 | 17 | [dependencies] 18 | rust = "1.89.0.*" 19 | 20 | [target.win-64.dependencies] 21 | vs2022_win-64 = ">=19.44.35207,<20" 22 | 23 | [target.linux-64.dependencies] 24 | libclang = "20.*" 25 | sysroot_linux-64 = ">=2.34" 26 | zlib = "==1.3.1" 27 | 28 | [target.linux-64.activation.env] 29 | LIBCLANG_PATH = "$CONDA_PREFIX/lib" 30 | 31 | [target.linux-aarch64.dependencies] 32 | libclang = "20.*" 33 | sysroot_linux-64 = ">=2.34" 34 | zlib = "==1.3.1" 35 | 36 | [target.linux-aarch64.activation.env] 37 | LIBCLANG_PATH = "$CONDA_PREFIX/lib" 38 | -------------------------------------------------------------------------------- /kson-tooling-lib/readme.md: -------------------------------------------------------------------------------- 1 | # Tooling API for KSON 2 | 3 | This subproject defines [KsonTooling.kt](src/commonMain/kotlin/org/kson/KsonTooling.kt), the public Kotlin Multiplatform interface for building tooling support for KSON. 4 | 5 | ## Available Tools 6 | 7 | The tooling API provides schema-aware information extraction at any location in a KSON document: 8 | 9 | - **Schema Information Retrieval** - Extract schema documentation, type information, constraints, and metadata for any position in a document 10 | - **Completion Suggestions** - Get valid property names, enum values, and type-appropriate suggestions based on the schema 11 | - **Document Path Navigation** - Navigate through KSON documents using schema-aware path building 12 | 13 | These capabilities can be used to build IDEs, linters, documentation generators, CLI tools, web editors, or any other tooling that needs schema-aware document analysis. -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/src/stubs/minimatch.stub.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stub for minimatch module in browser builds. 3 | * 4 | * The vscode-languageclient library imports minimatch for features like: 5 | * - DiagnosticFeature (diagnostic pull mode) 6 | * - NotebookDocumentSyncFeature 7 | * - FileOperationFeature (create/rename/delete file operations) 8 | * 9 | * Since we don't use these features in the browser client, we provide 10 | * a minimal stub to prevent the full minimatch library from being bundled. 11 | */ 12 | 13 | export class Minimatch { 14 | constructor(pattern: string, options?: any) { 15 | console.warn('minimatch stub called - this feature is not supported in browser mode'); 16 | } 17 | 18 | match(path: string): boolean { 19 | return false; 20 | } 21 | } 22 | 23 | // Default export for different import styles 24 | export default { 25 | Minimatch 26 | }; -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/OneOfValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonValue 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonSchema 7 | import org.kson.schema.JsonSchemaValidator 8 | 9 | class OneOfValidator(private val oneOf: List) : JsonSchemaValidator { 10 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 11 | val hasExactlyOneOf = oneOf.count { 12 | val oneOfMessageSink = MessageSink() 13 | it.isValid(ksonValue, oneOfMessageSink) 14 | } == 1 15 | if (!hasExactlyOneOf) { 16 | // schema todo maybe merge in some of the errors we found? 17 | messageSink.error(ksonValue.location, MessageType.SCHEMA_ONE_OF_VALIDATION_FAILED.create()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | base 3 | } 4 | 5 | tasks { 6 | val npmInstall = register("npmInstall") { 7 | command=listOf("npm", "install") 8 | doNotTrackState("npm already tracks its own state") 9 | } 10 | 11 | register("npm_run_compile") { 12 | command=listOf("npm", "run", "compile") 13 | dependsOn(":kson-lib:jsNodeProductionLibraryDistribution") 14 | dependsOn(":kson-tooling-lib:jsNodeProductionLibraryDistribution") 15 | dependsOn(npmInstall) 16 | } 17 | 18 | register("npm_run_test") { 19 | command=listOf("npm", "run", "test") 20 | dependsOn("npm_run_compile") 21 | } 22 | 23 | check { 24 | dependsOn("npm_run_test") 25 | } 26 | 27 | clean { 28 | delete("out") 29 | delete("node_modules") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib-python/uvw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Simple uv wrapper that auto-installs uv locally if needed 4 | 5 | UV_DIR="$(pwd)/.uv" 6 | UV_BIN="$UV_DIR/uv" 7 | 8 | # Install uv locally if not present 9 | if [ ! -f "$UV_BIN" ]; then 10 | echo "Installing uv locally to $UV_DIR..." 11 | 12 | # Use uv's official installation script with custom install location 13 | export UV_UNMANAGED_INSTALL="$UV_DIR" 14 | 15 | if command -v curl >/dev/null 2>&1; then 16 | curl -fsSL https://astral.sh/uv/install.sh | bash 17 | elif command -v wget >/dev/null 2>&1; then 18 | wget -qO- https://astral.sh/uv/install.sh | bash 19 | else 20 | echo "Please install curl or wget" >&2 21 | exit 1 22 | fi 23 | 24 | if [ ! -f "$UV_BIN" ]; then 25 | echo "Failed to install uv" >&2 26 | exit 1 27 | fi 28 | fi 29 | 30 | # Execute uv with all arguments 31 | exec "$UV_BIN" "$@" 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/ContainsValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonList 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonArrayValidator 7 | import org.kson.schema.JsonSchema 8 | 9 | class ContainsValidator(private val containsSchema: JsonSchema) : JsonArrayValidator() { 10 | override fun validateArray(node: KsonList, messageSink: MessageSink) { 11 | val foundMatchingElement = node.elements.any { element -> 12 | val containsMessageSink = MessageSink() 13 | containsSchema.validate(element, containsMessageSink) 14 | !containsMessageSink.hasMessages() 15 | } 16 | if (!foundMatchingElement) { 17 | messageSink.error(node.location, MessageType.SCHEMA_CONTAINS_VALIDATION_FAILED.create()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.js.generate.executable.default=false 3 | org.gradle.jvmargs=-Xmx2048m 4 | 5 | # Explicitly opt-in to the Kotlin stlib that Gradle automatically provides 6 | # to quiet the warning around this from our :tooling:jetbrains subproject. 7 | # See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library 8 | # for details. 9 | # 10 | # NOTE: it should be possible to set this in the :tooling:jetbrains subproject, closer to 11 | # the code that demands it, but there is currently a bug in how the warning detects this 12 | # setting (https://github.com/JetBrains/intellij-platform-gradle-plugin/issues/1789) 13 | kotlin.stdlib.default.dependency = true 14 | 15 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 16 | # disable the DokkaV2 welcome message in the build 17 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 18 | 19 | org.gradle.console=verbose 20 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/schema/createSchemaProvider.node.ts: -------------------------------------------------------------------------------- 1 | import {SchemaProvider} from './SchemaProvider.js'; 2 | import {FileSystemSchemaProvider} from './FileSystemSchemaProvider'; 3 | import {URI} from "vscode-uri"; 4 | 5 | /** 6 | * Node.js-specific schema provider factory. 7 | * Creates {@link FileSystemSchemaProvider} with file system access. 8 | */ 9 | export async function createSchemaProvider( 10 | workspaceRootUri: URI | undefined, 11 | logger: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void } 12 | ): Promise { 13 | try { 14 | logger.info('Running in Node.js environment, using FileSystemSchemaProvider'); 15 | return new FileSystemSchemaProvider(workspaceRootUri || null, logger); 16 | } catch (error) { 17 | logger.error(`Failed to create FileSystemSchemaProvider: ${error}`); 18 | return undefined; 19 | } 20 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kson-lsp-extensions", 3 | "//": "[[kson-version-num]]", 4 | "version": "0.3.0-dev.0", 5 | "private": true, 6 | "description": "KSON Language Server Protocol Extensions", 7 | "workspaces": [ 8 | "shared", 9 | "vscode", 10 | "monaco" 11 | ], 12 | "scripts": { 13 | "buildMonaco": "npm run compile && npm run build -w monaco", 14 | "buildVSCode": "npm run compile && npm run packagePlugin -w vscode", 15 | "buildPlugins": "npm run buildMonaco && npm run buildVSCode", 16 | "compile": "npm run compile -w shared && npm run compile -w vscode", 17 | "clean": "npm run clean --workspaces --if-present", 18 | "test": "npm run compile && npm run test --workspaces --if-present", 19 | "vscode": "npm run compile && npm run vscode -w vscode", 20 | "monaco": "npm run compile && npm run dev -w monaco" 21 | }, 22 | "devDependencies": { 23 | "typescript": "^5.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/logo/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # assuming the root image is kson_logo_transparent.svg we can regenerate the rest 4 | 5 | # First, add a background to the transparent SVG 6 | # Assuming the `g` node's last attirbute is `id="layer1"`, this will append a new rectangle (background) 7 | # just inside the group 8 | sed '/id="layer1">$/a\ ' kson_logo_transparent.svg > kson_logo_blue.svg 9 | 10 | # Now create png versions of the transparent and blue svgs 11 | inkscape kson_logo_transparent.svg --export-type=png --export-filename=kson_logo_transparent.png 12 | inkscape kson_logo_transparent.svg --export-type=png --export-width=800 --export-height=800 --export-filename=kson_logo_transparent_800x800.png 13 | inkscape kson_logo_blue.svg --export-type=png --export-filename=kson_logo_blue.png 14 | inkscape kson_logo_blue.svg --export-type=png --export-width=800 --export-height=800 --export-filename=kson_logo_blue_800x800.png 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/IfValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonValue 4 | import org.kson.parser.MessageSink 5 | import org.kson.schema.JsonSchema 6 | import org.kson.schema.JsonSchemaValidator 7 | 8 | class IfValidator(private val ifSchema: JsonSchema?, private val thenSchema: JsonSchema?, private val elseSchema: JsonSchema?) : 9 | JsonSchemaValidator { 10 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 11 | if (ifSchema == null) { 12 | return 13 | } 14 | 15 | val tmpMessageSink = MessageSink() 16 | if (ifSchema.isValid(ksonValue, tmpMessageSink)) { 17 | // if condition is true, run then schema if it exists 18 | thenSchema?.validate(ksonValue, messageSink) 19 | } else { 20 | // if condition is false, run else schema if it exists 21 | elseSchema?.validate(ksonValue, messageSink) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/RequiredValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonObject 4 | import org.kson.value.KsonString 5 | import org.kson.parser.MessageSink 6 | import org.kson.parser.messages.MessageType 7 | import org.kson.schema.JsonObjectValidator 8 | 9 | class RequiredValidator(private val required: List) : JsonObjectValidator() { 10 | override fun validateObject(node: KsonObject, messageSink: MessageSink) { 11 | val propertyNames = node.propertyMap.keys 12 | val missingProperties = required.filter { !propertyNames.contains(it.value) } 13 | if (missingProperties.isNotEmpty()) { 14 | val missingPropertyNames = missingProperties.joinToString(", ") { it.value } 15 | messageSink.error( 16 | node.location.trimToFirstLine(), 17 | MessageType.SCHEMA_REQUIRED_PROPERTY_MISSING.create(missingPropertyNames) 18 | ) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/schema/validators/ConstValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.schema.JsonSchemaTest 4 | import org.kson.parser.messages.MessageType.SCHEMA_VALUE_NOT_EQUAL_TO_CONST 5 | import kotlin.test.Test 6 | import kotlin.test.assertContains 7 | 8 | class ConstValidatorTest : JsonSchemaTest { 9 | @Test 10 | fun testErrorMessaging() { 11 | val constValue = "the const value" 12 | val errors = assertKsonSchemaErrors( 13 | """ 14 | isConst: "this value is wrong" 15 | """.trimIndent(), 16 | """ 17 | properties: 18 | isConst: 19 | const: "$constValue" 20 | """.trimIndent(), 21 | listOf(SCHEMA_VALUE_NOT_EQUAL_TO_CONST) 22 | ) 23 | 24 | // ensure the error message refers to the required constant value 25 | assertContains(errors[0].message.toString(), constValue) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/createCommandExecutor.browser.ts: -------------------------------------------------------------------------------- 1 | import {Connection} from 'vscode-languageserver'; 2 | import {KsonDocumentsManager} from '../document/KsonDocumentsManager.js'; 3 | import {FormattingService} from '../features/FormattingService.js'; 4 | import type {KsonSettings} from '../KsonSettings.js'; 5 | import {CommandExecutor} from './CommandExecutor.browser.js'; 6 | 7 | /** 8 | * Browser-specific command executor factory. 9 | * Creates CommandExecutor without file system support. 10 | */ 11 | export function createCommandExecutor( 12 | connection: Connection, 13 | documentManager: KsonDocumentsManager, 14 | formattingService: FormattingService, 15 | getConfiguration: () => Required, 16 | workspaceRoot: string | null = null 17 | ): CommandExecutor { 18 | return new CommandExecutor( 19 | connection, 20 | documentManager, 21 | formattingService, 22 | getConfiguration, 23 | workspaceRoot 24 | ); 25 | } -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/createCommandExecutor.node.ts: -------------------------------------------------------------------------------- 1 | import {Connection} from 'vscode-languageserver'; 2 | import {KsonDocumentsManager} from '../document/KsonDocumentsManager.js'; 3 | import {FormattingService} from '../features/FormattingService.js'; 4 | import type {KsonSettings} from '../KsonSettings.js'; 5 | import {CommandExecutor} from './CommandExecutor.node.js'; 6 | 7 | /** 8 | * Node.js-specific command executor factory. 9 | * Creates CommandExecutor with full file system support. 10 | */ 11 | export function createCommandExecutor( 12 | connection: Connection, 13 | documentManager: KsonDocumentsManager, 14 | formattingService: FormattingService, 15 | getConfiguration: () => Required, 16 | workspaceRoot: string | null = null 17 | ): CommandExecutor { 18 | return new CommandExecutor( 19 | connection, 20 | documentManager, 21 | formattingService, 22 | getConfiguration, 23 | workspaceRoot 24 | ); 25 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/editor/KsonQuoteMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | class KsonQuoteMatcherTest : KsonEditorActionTest() { 4 | /** 5 | * Sanity check that [KsonQuoteHandler] is correctly hooked up for auto-inserting close quotes 6 | * This is the inverse operation to what is tested in [KsonBackspaceHandlerDelegateTest.testDeleteEmptyQuotePairs] 7 | */ 8 | fun testAutoInsert() { 9 | doCharTest( 10 | "", 11 | '"', 12 | "\"\"" 13 | ) 14 | 15 | doCharTest( 16 | "", 17 | '\'', 18 | "''" 19 | ) 20 | } 21 | 22 | fun testManualClose() { 23 | doCharTest( 24 | "\"", 25 | '"', 26 | "\"\"" 27 | ) 28 | 29 | doCharTest( 30 | "key: \"unclosed", 31 | '"', 32 | "key: \"unclosed\"" 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/MultipleOfValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonNumber 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonNumberValidator 7 | import kotlin.math.abs 8 | import kotlin.math.max 9 | 10 | class MultipleOfValidator(private val multipleOf: Double) : JsonNumberValidator() { 11 | override fun validateNumber(node: KsonNumber, messageSink: MessageSink) { 12 | val number = node.value.asDouble 13 | if (multipleOf != 0.0) { 14 | val remainder = number % multipleOf 15 | val epsilon = 1e-10 * max(abs(number), abs(multipleOf)) 16 | if (abs(remainder) > epsilon && abs(remainder - multipleOf) > epsilon) { 17 | messageSink.error( 18 | node.location, 19 | MessageType.SCHEMA_VALUE_MUST_BE_MULTIPLE_OF.create(multipleOf.toString()) 20 | ) 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/parser/messages/MessagesTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.parser.messages 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertContains 5 | import kotlin.test.assertFailsWith 6 | 7 | class MessagesTest { 8 | @Test 9 | fun testFormat() { 10 | // sanity check formatting is hooked up correctly by verifying we can find our 11 | // "tagNameForTest" embedded in the formatted string 12 | // 13 | // (NOTE: this naturally relies on assumption that EMBED_BLOCK_NO_CLOSE's formatted 14 | // message will always refer to its embed tag. Apologies if that changes and this fails) 15 | val message = MessageType.EMBED_BLOCK_NO_CLOSE.create("embedDelimiter") 16 | assertContains(message.toString(), "embedDelimiter") 17 | } 18 | 19 | @Test 20 | fun testFormatNullArgs() { 21 | assertFailsWith(IllegalArgumentException::class, "should blow up on null argument") { 22 | MessageType.EMBED_BLOCK_NO_CLOSE.create(null) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib-python/uvw.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem Simple uv wrapper for Windows that auto-installs uv locally if needed 4 | 5 | set UV_DIR=%cd%\.uv 6 | set UV_BIN=%UV_DIR%\uv.exe 7 | 8 | rem Install uv locally if not present 9 | if not exist "%UV_BIN%" ( 10 | echo Installing uv locally to %UV_DIR%... 11 | 12 | rem Use uv's official installation script with custom install location 13 | set UV_UNMANAGED_INSTALL=%UV_DIR% 14 | 15 | rem Use pwsh if available, because legacy powershell causes problems when launched from pwsh! 16 | where pwsh >nul 2>&1 17 | if %errorlevel% equ 0 ( 18 | pwsh -NoProfile -ExecutionPolicy ByPass -Command "iwr -useb https://astral.sh/uv/install.ps1 | iex" 19 | ) else ( 20 | powershell -NoProfile -ExecutionPolicy ByPass -Command "iwr -useb https://astral.sh/uv/install.ps1 | iex" 21 | ) 22 | 23 | if not exist "%UV_BIN%" ( 24 | echo Failed to install uv 25 | exit /b 1 26 | ) 27 | ) 28 | 29 | rem Execute uv with all arguments 30 | "%UV_BIN%" %* 31 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/suite/assert.ts: -------------------------------------------------------------------------------- 1 | // Browser-compatible assert implementation 2 | export const assert = { 3 | strictEqual(actual: T, expected: T, message?: string): void { 4 | if (actual !== expected) { 5 | throw new Error(message || `Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); 6 | } 7 | }, 8 | 9 | ok(value: any, message?: string): void { 10 | if (!value) { 11 | throw new Error(message || `Expected truthy value but got ${JSON.stringify(value)}`); 12 | } 13 | }, 14 | 15 | fail(message?: string): void { 16 | throw new Error(message || 'Assertion failed'); 17 | }, 18 | 19 | deepStrictEqual(actual: T, expected: T, message?: string): void { 20 | const actualStr = JSON.stringify(actual); 21 | const expectedStr = JSON.stringify(expected); 22 | if (actualStr !== expectedStr) { 23 | throw new Error(message || `Expected ${expectedStr} but got ${actualStr}`); 24 | } 25 | } 26 | }; -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/folding/KsonFoldingTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.folding 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 4 | 5 | class KsonFoldingTest : BasePlatformTestCase() { 6 | override fun getTestDataPath(): String = "src/test/resources/testData/folding" 7 | 8 | override fun setUp() { 9 | super.setUp() 10 | myFixture.testDataPath = testDataPath 11 | } 12 | 13 | fun testDashListFolding() { 14 | doTest() 15 | } 16 | 17 | fun testBracketListFolding() { 18 | doTest() 19 | } 20 | 21 | fun testAngleListFolding() { 22 | doTest() 23 | } 24 | 25 | fun testEmbedBlockFolding() { 26 | doTest() 27 | } 28 | 29 | fun testSingleLineShouldNotFold() { 30 | doTest() 31 | } 32 | 33 | fun testObjectFolding() { 34 | doTest() 35 | } 36 | 37 | private fun doTest() { 38 | val testName = getTestName(true) 39 | myFixture.testFolding("$testDataPath/$testName.kson") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/resources/kson/example.kson: -------------------------------------------------------------------------------- 1 | # Example KSON file 2 | # This is a comment 3 | 4 | # Simple key-value pairs 5 | name: "John Doe" 6 | age: 30 7 | active: true 8 | 9 | # Nested objects 10 | address: { 11 | street: "123 Main St" 12 | city: "New York" 13 | zipCode: "10001" 14 | } 15 | 16 | # Arrays 17 | hobbies: [ 18 | "reading" 19 | "swimming" 20 | "coding" 21 | ] 22 | 23 | # Mixed array 24 | mixedData: [ 25 | 42 26 | "hello" 27 | true 28 | { nested: "value" } 29 | ] 30 | 31 | # Multi-line strings with embed blocks 32 | description: $ 33 | This is a multi-line 34 | string example in KSON 35 | $$ 36 | 37 | # Code embed block with language tag 38 | script: %javascript 39 | function hello() { 40 | console.log("Hello from KSON!"); 41 | } 42 | %% 43 | 44 | # Complex nested structure 45 | project: { 46 | name: "KSON Editor" 47 | version: "1.0.0" 48 | dependencies: { 49 | "monaco-editor": "^0.45.0" 50 | "vscode-languageclient": "^9.0.0" 51 | } 52 | scripts: { 53 | build: "npm run compile" 54 | test: "npm test" 55 | } 56 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/editor/KsonPairedBraceMatcherTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | class KsonPairedBraceMatcherTest : KsonEditorActionTest() { 4 | /** 5 | * Sanity check that [KsonPairedBraceMatcher] is correctly hooked up 6 | */ 7 | fun testAutoInsert() { 8 | withConfigSetting(ConfigProperty.AUTOINSERT_PAIR_BRACKET(), true) { 9 | doCharTest( 10 | "", 11 | '{', 12 | "{}" 13 | ) 14 | 15 | doCharTest( 16 | "", 17 | '[', 18 | "[]" 19 | ) 20 | } 21 | 22 | withConfigSetting(ConfigProperty.AUTOINSERT_PAIR_BRACKET(), false) { 23 | doCharTest( 24 | "", 25 | '{', 26 | "{" 27 | ) 28 | 29 | doCharTest( 30 | "", 31 | '[', 32 | "[" 33 | ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kson/monaco-editor", 3 | "version": "1.0.0", 4 | "description": "Monaco editor with KSON language support and LSP integration", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "test": "playwright test", 11 | "test:headed": "playwright test --headed" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@codingame/esbuild-import-meta-url-plugin": "^1.0.3", 17 | "@codingame/monaco-vscode-typescript-basics-default-extension": "~18.1.0", 18 | "@codingame/monaco-vscode-javascript-default-extension": "~18.1.0", 19 | "@codingame/monaco-vscode-sql-default-extension": "~18.1.0", 20 | "@codingame/monaco-vscode-python-default-extension": "~18.1.0", 21 | "@kson/lsp-shared": "file:../shared", 22 | "monaco-editor-wrapper": "^6.9.0", 23 | "vscode-languageserver": "^9.0.1" 24 | }, 25 | "devDependencies": { 26 | "@playwright/test": "^1.53.2", 27 | "vite": "^7.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/UniqueItemsValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonList 4 | import org.kson.value.KsonValue 5 | import org.kson.parser.MessageSink 6 | import org.kson.parser.messages.MessageType 7 | import org.kson.schema.JsonArrayValidator 8 | 9 | class UniqueItemsValidator(private val uniqueItems: Boolean) : JsonArrayValidator() { 10 | override fun validateArray(node: KsonList, messageSink: MessageSink) { 11 | if (uniqueItems && !areItemsUnique(node.elements)) { 12 | messageSink.error(node.location, MessageType.SCHEMA_ARRAY_ITEMS_NOT_UNIQUE.create()) 13 | } 14 | } 15 | 16 | /** 17 | * Check if all items in a list are unique using JSON Schema equality semantics. 18 | */ 19 | private fun areItemsUnique(elements: List): Boolean { 20 | val seen = mutableSetOf() 21 | for (element in elements) { 22 | if (!seen.add(element)) { 23 | return false 24 | } 25 | } 26 | return true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/CommandType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum defining all available commands in the Kson Language Server 3 | */ 4 | export enum CommandType { 5 | /** 6 | * Format the document as plain Kson 7 | */ 8 | PLAIN_FORMAT = 'kson.plainFormat', 9 | 10 | /** 11 | * Format the document as delimited Kson 12 | */ 13 | DELIMITED_FORMAT = 'kson.delimitedFormat', 14 | 15 | /** 16 | * Format the document as compact Kson 17 | */ 18 | COMPACT_FORMAT = 'kson.compactFormat', 19 | 20 | /** 21 | * Format the document as classic Kson 22 | */ 23 | CLASSIC_FORMAT = 'kson.classicFormat', 24 | 25 | /** 26 | * Associate a schema with the current document 27 | */ 28 | ASSOCIATE_SCHEMA = 'kson.associateSchema', 29 | 30 | /** 31 | * Remove schema association from the current document 32 | */ 33 | REMOVE_SCHEMA = 'kson.removeSchema' 34 | } 35 | 36 | /** 37 | * Get all available command IDs 38 | */ 39 | export function getAllCommandIds(): string[] { 40 | return Object.values(CommandType); 41 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/src/config/clientOptions.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode'; 2 | import { 3 | LanguageClientOptions, 4 | ErrorAction, 5 | CloseAction, 6 | } from 'vscode-languageclient'; 7 | 8 | // Create shared client options 9 | export const createClientOptions = (outputChannel: vscode.OutputChannel): LanguageClientOptions => { 10 | 11 | return { 12 | documentSelector: [ 13 | {scheme: 'file', language: 'kson'}, 14 | {scheme: 'untitled', language: 'kson'} 15 | ], 16 | synchronize: { 17 | /** 18 | * TODO - Even though this setting is deprecated it is the easiest way to get configuration going. 19 | * We should find a way in the future to replace this with the /pull model. 20 | */ 21 | configurationSection: 'kson', 22 | fileEvents: vscode.workspace.createFileSystemWatcher('**/*.kson') 23 | }, 24 | outputChannel, 25 | errorHandler: { 26 | error: () => ({action: ErrorAction.Continue}), 27 | closed: () => ({action: CloseAction.DoNotRestart}) 28 | } 29 | }; 30 | } -------------------------------------------------------------------------------- /kson-lib/pixi.lock: -------------------------------------------------------------------------------- 1 | version: 6 2 | environments: 3 | default: 4 | channels: 5 | - url: https://conda.anaconda.org/conda-forge/ 6 | packages: 7 | win-64: 8 | - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_31.conda 9 | - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda 10 | packages: 11 | - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_31.conda 12 | sha256: c0a380608afc9ad98b1e261fe55702b72e33f80d372f358075c5dea3d185816f 13 | md5: 0bf228856c72261e5bbd9530002176f2 14 | depends: 15 | - vswhere 16 | constrains: 17 | - vs_win-64 2022.14 18 | track_features: 19 | - vc14 20 | license: BSD-3-Clause 21 | license_family: BSD 22 | size: 21054 23 | timestamp: 1753739201422 24 | - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda 25 | sha256: b72270395326dc56de9bd6ca82f63791b3c8c9e2b98e25242a9869a4ca821895 26 | md5: f622897afff347b715d046178ad745a5 27 | depends: 28 | - __win 29 | license: MIT 30 | license_family: MIT 31 | size: 238764 32 | timestamp: 1745560912727 33 | -------------------------------------------------------------------------------- /tooling/cli/pixi.lock: -------------------------------------------------------------------------------- 1 | version: 6 2 | environments: 3 | default: 4 | channels: 5 | - url: https://conda.anaconda.org/conda-forge/ 6 | packages: 7 | win-64: 8 | - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_31.conda 9 | - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda 10 | packages: 11 | - conda: https://conda.anaconda.org/conda-forge/win-64/vs2022_win-64-19.44.35207-ha74f236_31.conda 12 | sha256: c0a380608afc9ad98b1e261fe55702b72e33f80d372f358075c5dea3d185816f 13 | md5: 0bf228856c72261e5bbd9530002176f2 14 | depends: 15 | - vswhere 16 | constrains: 17 | - vs_win-64 2022.14 18 | track_features: 19 | - vc14 20 | license: BSD-3-Clause 21 | license_family: BSD 22 | size: 21054 23 | timestamp: 1753739201422 24 | - conda: https://conda.anaconda.org/conda-forge/noarch/vswhere-3.1.7-h40126e0_1.conda 25 | sha256: b72270395326dc56de9bd6ca82f63791b3c8c9e2b98e25242a9869a4ca821895 26 | md5: f622897afff347b715d046178ad745a5 27 | depends: 28 | - __win 29 | license: MIT 30 | license_family: MIT 31 | size: 238764 32 | timestamp: 1745560912727 33 | -------------------------------------------------------------------------------- /lib-python/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include build_backend.py 2 | include pyproject.toml 3 | include README.md 4 | recursive-include src *.py 5 | recursive-include src *.c 6 | 7 | # Include necessary Gradle files from kson-sdist 8 | recursive-include kson-sdist *.kts *.kt *.properties *.md 9 | # Include Python source files from kson-sdist 10 | recursive-include kson-sdist *.py 11 | include kson-sdist/.circleci/config.kson 12 | include kson-sdist/gradlew 13 | include kson-sdist/gradlew.bat 14 | include kson-sdist/buildSrc/gradlew 15 | include kson-sdist/buildSrc/gradlew.bat 16 | include kson-sdist/gradle/wrapper/gradle-wrapper.jar 17 | include kson-sdist/gradle/wrapper/gradle-wrapper.properties 18 | include kson-sdist/buildSrc/gradle/wrapper/gradle-wrapper.jar 19 | include kson-sdist/buildSrc/gradle/wrapper/gradle-wrapper.properties 20 | 21 | # Explicitly exclude all build artifacts 22 | prune kson-sdist/**/.gradle 23 | prune kson-sdist/**/build 24 | prune kson-sdist/**/.kotlin 25 | global-exclude __pycache__ 26 | global-exclude *.py[co] 27 | global-exclude .DS_Store 28 | global-exclude *.class 29 | global-exclude *.lock 30 | global-exclude *.bin 31 | global-exclude *.hprof 32 | global-exclude *.log -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/stdlibx/collections/Immutable.kt: -------------------------------------------------------------------------------- 1 | package org.kson.stdlibx.collections 2 | 3 | /** 4 | * Some useful immutable collection extensions from https://stackoverflow.com/a/37936456 5 | */ 6 | 7 | class ImmutableList private constructor(private val inner: List) : List by inner { 8 | companion object { 9 | fun create(inner: List) = if (inner is ImmutableList) { 10 | inner 11 | } else { 12 | ImmutableList(inner.toList()) 13 | } 14 | } 15 | } 16 | 17 | class ImmutableMap private constructor(private val inner: Map) : Map by inner { 18 | companion object { 19 | fun create(inner: Map) = if (inner is ImmutableMap) { 20 | inner 21 | } else { 22 | ImmutableMap(hashMapOf(*inner.toList().toTypedArray())) 23 | } 24 | } 25 | } 26 | 27 | fun Map.toImmutableMap(): ImmutableMap = ImmutableMap.create(this) 28 | fun List.toImmutableList(): ImmutableList = ImmutableList.create(this) 29 | fun Set.toImmutableList(): ImmutableList = ImmutableList.create(this.toList()) 30 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/editor/KsonCompletionContributor.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor 4 | import com.intellij.codeInsight.completion.CompletionInitializationContext 5 | import com.intellij.codeInsight.completion.CompletionType 6 | import com.intellij.patterns.PlatformPatterns.psiElement 7 | import org.kson.jetbrains.parser.elem 8 | import org.kson.parser.TokenType 9 | 10 | /** 11 | * Provides completion for language tags in KSON embed blocks. 12 | * Supports both custom language providers and built-in injectable languages. 13 | */ 14 | class KsonCompletionContributor : CompletionContributor() { 15 | init { 16 | extend( 17 | CompletionType.BASIC, 18 | psiElement().withElementType(elem(TokenType.EMBED_TAG)), 19 | EmbedBlockLanguageListCompletionProvider() 20 | ) 21 | } 22 | 23 | override fun beforeCompletion(context: CompletionInitializationContext) { 24 | // Use a newline in the dummy identifier to prevent it from being parsed as part of the tag 25 | context.dummyIdentifier = "${CompletionInitializationContext.DUMMY_IDENTIFIER}\n" 26 | } 27 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/highlighter/KsonSemanticHighlightAnnotator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.highlighter 2 | 3 | import com.intellij.lang.annotation.AnnotationHolder 4 | import com.intellij.lang.annotation.Annotator 5 | import com.intellij.lang.annotation.HighlightSeverity 6 | import com.intellij.openapi.project.DumbAware 7 | import com.intellij.psi.PsiElement 8 | import org.kson.jetbrains.parser.elem 9 | import org.kson.parser.ParsedElementType 10 | 11 | /** 12 | * [KsonSemanticHighlightAnnotator] provides semantic highlighting for Kson files. 13 | * 14 | * Since we don't require any indices we can implement [DumbAware] 15 | */ 16 | class KsonSemanticHighlightAnnotator : Annotator, DumbAware { 17 | override fun annotate(element: PsiElement, holder: AnnotationHolder) { 18 | // Implement syntax highlighter that distinguishes regular strings from object keys. 19 | if (elem(ParsedElementType.OBJECT_KEY) == element.node.elementType){ 20 | holder.newSilentAnnotation(HighlightSeverity.INFORMATION).textAttributes( 21 | KsonSyntaxHighlighter.getTextAttributesKey(KsonSyntaxHighlighter.KsonColorTag.KSON_OBJECT_KEY) 22 | ).create() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/util/DocumentExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.util 2 | 3 | import com.intellij.openapi.editor.Document 4 | import com.intellij.openapi.util.TextRange 5 | import org.kson.tools.IndentFormatter 6 | import org.kson.tools.IndentType 7 | 8 | /** 9 | * Calculates the indentation level of a specific line in the document. 10 | * 11 | * @param lineNumber The line number to analyze (0-based) 12 | * @param indentType The type of indentation to consider (e.g., spaces or tabs) 13 | * @return The number of indentation levels for the specified line 14 | */ 15 | fun Document.getLineIndentLevel(lineNumber: Int, indentType: IndentType): Int { 16 | val prevLine = if (lineNumber > 0) { 17 | val lineStart = this.getLineStartOffset(lineNumber - 1) 18 | val lineEnd = this.getLineEndOffset(lineNumber - 1) 19 | this.getText(TextRange(lineStart, lineEnd)) 20 | } else { 21 | "" 22 | } 23 | 24 | val lineStart = this.getLineStartOffset(lineNumber) 25 | val lineEnd = this.getLineEndOffset(lineNumber) 26 | val currentLine = this.getText(TextRange(lineStart, lineEnd)) 27 | 28 | return IndentFormatter(indentType).getCurrentLineIndentLevel(prevLine, currentLine) 29 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/psi/KsonEmbedBlock.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.psi 2 | 3 | import com.intellij.lang.ASTNode 4 | import com.intellij.psi.util.elementType 5 | import org.kson.jetbrains.parser.elem 6 | import org.kson.parser.TokenType 7 | import org.kson.parser.behavior.embedblock.EmbedDelim 8 | import org.kson.stdlibx.exceptions.ShouldNotHappenException 9 | 10 | class KsonEmbedBlock(node: ASTNode) : KsonPsiElement(node) { 11 | val embedBlockTag: String 12 | get() = node.findChildByType(elem(TokenType.EMBED_TAG))?.text ?: "" 13 | 14 | val embedDelim: EmbedDelim 15 | get() = getDelim(this) 16 | 17 | val embedContent: KsonEmbedContent? 18 | get() = node.findChildByType(elem(TokenType.EMBED_CONTENT))?.psi as? KsonEmbedContent 19 | 20 | 21 | companion object { 22 | private fun getDelim(host: KsonEmbedBlock): EmbedDelim { 23 | val embedDelimText = 24 | host.children.find { it.elementType == elem(TokenType.EMBED_OPEN_DELIM) }?.text 25 | ?: throw ShouldNotHappenException("since this element got parsed as embed block we expect an EMBED_OPEN_DELIM") 26 | return EmbedDelim.fromString(embedDelimText) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kson/lsp-shared", 3 | "version": "0.0.1", 4 | "description": "Shared configurations and utilities for KSON LSP extensions", 5 | "type": "module", 6 | "main": "./dist/src/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/src/index.d.ts", 11 | "default": "./dist/src/index.js" 12 | }, 13 | "./config": { 14 | "types": "./dist/src/config/index.d.ts", 15 | "default": "./dist/src/config/index.js" 16 | }, 17 | "./connection": { 18 | "types": "./dist/src/connection/index.d.ts", 19 | "default": "./dist/src/connection/index.js" 20 | } 21 | }, 22 | "scripts": { 23 | "compile": "tsc", 24 | "clean": "rm -rf dist", 25 | "generate:tmlanguage": "tsx scripts/generate-tm-embed-block.ts" 26 | }, 27 | "keywords": [ 28 | "kson", 29 | "lsp", 30 | "shared" 31 | ], 32 | "dependencies": { 33 | "kson-language-server": "file:../../language-server-protocol", 34 | "vscode-languageclient": "^9.0.1", 35 | "vscode-languageserver": "^9.0.1" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20.0.0", 39 | "tsx": "^4.20.3", 40 | "typescript": "^5.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/parser/behavior/StringUnquoted.kt: -------------------------------------------------------------------------------- 1 | package org.kson.parser.behavior 2 | 3 | /** 4 | * Behaviors and rules around unquoted Kson strings 5 | */ 6 | object StringUnquoted { 7 | 8 | private val reservedKeywords = setOf("true", "false", "null") 9 | 10 | /** 11 | * Returns true if the given string may be used without quotes in a Kson document 12 | */ 13 | fun isUnquotable(str: String): Boolean { 14 | return !reservedKeywords.contains(str) && str.isNotBlank() && str.withIndex().all { (index, letter) -> 15 | if (index == 0) { 16 | isUnquotedStartChar(letter) 17 | } else { 18 | isUnquotedBodyChar(letter) 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * Returns true if [ch] is a legal first [Char] for an unquoted Kson string 25 | */ 26 | fun isUnquotedStartChar(ch: Char?): Boolean { 27 | ch ?: return false 28 | return ch.isLetter() || (ch == '_') 29 | } 30 | 31 | /** 32 | * Returns true if [ch] is a legal [Char] for the body of an unquoted Kson string 33 | */ 34 | fun isUnquotedBodyChar(ch: Char?): Boolean { 35 | ch ?: return false 36 | return isUnquotedStartChar(ch) || ch.isDigit() 37 | } 38 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/schema/validators/AllOfValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.parser.messages.MessageType.* 4 | import org.kson.schema.JsonSchemaTest 5 | import kotlin.test.Test 6 | 7 | class AllOfValidatorTest : JsonSchemaTest { 8 | @Test 9 | fun testAllOfValidationErrors() { 10 | assertKsonSchemaErrors( 11 | """ 12 | description: 99 13 | think: false 14 | """.trimIndent(), 15 | """ 16 | allOf: 17 | - properties: 18 | description: 19 | type: string 20 | . 21 | thing: 22 | type: number 23 | 24 | - required: [other_thing] 25 | properties: 26 | description: 27 | type: string 28 | """.trimIndent(), 29 | listOf( 30 | // description is wrong type according to both schemas 31 | SCHEMA_VALUE_TYPE_MISMATCH, 32 | // missing 'other_thing' required by second schema 33 | SCHEMA_REQUIRED_PROPERTY_MISSING) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/util/PsiFileExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.util 2 | 3 | import com.intellij.application.options.CodeStyle 4 | import com.intellij.psi.PsiElement 5 | import com.intellij.psi.PsiFile 6 | import com.intellij.psi.tree.IElementType 7 | import com.intellij.psi.util.elementType 8 | import com.intellij.psi.util.findParentInFile 9 | import org.kson.tools.IndentType 10 | 11 | /** 12 | * Returns true if the given [offset] within this [PsiFile] lands inside a [PsiElement] of one of the types 13 | * given in [elements] 14 | */ 15 | fun PsiFile.hasElementAtOffset(offset: Int, elements: Set): Boolean { 16 | val elemAtFirstDelimChar = this.findElementAt(offset) ?: return false 17 | val parentFound = elemAtFirstDelimChar.findParentInFile(true) { 18 | elements.contains(it.elementType) 19 | } 20 | return parentFound != null 21 | } 22 | 23 | /** 24 | * Gets the indent type (tabs or spaces) configured for this file 25 | */ 26 | fun PsiFile.getIndentType(): IndentType { 27 | val indentOptions = CodeStyle.getIndentOptions(this) 28 | return if (indentOptions.USE_TAB_CHARACTER) { 29 | IndentType.Tab() 30 | } else { 31 | IndentType.Space(indentOptions.INDENT_SIZE) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/editor/KsonPairedBraceMatcher.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | import com.intellij.lang.BracePair 4 | import com.intellij.lang.PairedBraceMatcher 5 | import com.intellij.psi.PsiFile 6 | import com.intellij.psi.tree.IElementType 7 | import org.kson.jetbrains.parser.elem 8 | import org.kson.parser.TokenType 9 | 10 | private val bracePairs = arrayOf( 11 | BracePair( 12 | elem(TokenType.CURLY_BRACE_L), 13 | elem(TokenType.CURLY_BRACE_R), 14 | true 15 | ), 16 | BracePair( 17 | elem(TokenType.SQUARE_BRACKET_L), 18 | elem(TokenType.SQUARE_BRACKET_R), 19 | true 20 | ), 21 | BracePair( 22 | elem(TokenType.ANGLE_BRACKET_L), 23 | elem(TokenType.ANGLE_BRACKET_R), 24 | true 25 | ) 26 | ) 27 | 28 | class KsonPairedBraceMatcher : PairedBraceMatcher { 29 | 30 | override fun getPairs(): Array { 31 | return bracePairs 32 | } 33 | 34 | override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?): Boolean { 35 | return true 36 | } 37 | 38 | override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int): Int { 39 | return openingBraceOffset 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/schema/SchemaProvider.ts: -------------------------------------------------------------------------------- 1 | import {TextDocument} from 'vscode-languageserver-textdocument'; 2 | import {DocumentUri} from 'vscode-languageserver'; 3 | 4 | /** 5 | * Interface for providing schemas in different environments (Node.js, Browser). 6 | * This abstraction allows the schema system to work without direct file system access. 7 | */ 8 | export interface SchemaProvider { 9 | /** 10 | * Get the schema document for a given KSON document URI. 11 | * 12 | * @param documentUri The URI of the KSON document 13 | * @returns TextDocument containing the schema, or undefined if no schema is available 14 | */ 15 | getSchemaForDocument(documentUri: DocumentUri): TextDocument | undefined; 16 | 17 | /** 18 | * Reload the schema configuration. 19 | * Should be called when configuration changes are detected. 20 | */ 21 | reload(): void; 22 | } 23 | 24 | /** 25 | * A no-op schema provider that returns undefined for all requests. 26 | * Used as a fallback when no schema configuration is available. 27 | */ 28 | export class NoOpSchemaProvider implements SchemaProvider { 29 | getSchemaForDocument(_documentUri: DocumentUri): TextDocument | undefined { 30 | return undefined; 31 | } 32 | 33 | reload(): void { 34 | // No-op 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tooling/jetbrains/gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = org.kson.jetbrains 5 | pluginName = KSON 6 | 7 | # [[kson-version-num]] 8 | pluginVersion = 0.3.0-SNAPSHOT 9 | 10 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 11 | # for insight into build numbers and IntelliJ Platform versions. 12 | pluginSinceBuild = 223.8 13 | pluginUntilBuild = 252.* 14 | 15 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html 16 | platformType = IC 17 | platformVersion = 2022.3.3 18 | 19 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 20 | # Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP 21 | platformPlugins = org.intellij.intelliLang 22 | # Example: platformBundledPlugins = com.intellij.java 23 | platformBundledPlugins = 24 | 25 | # Java language level used to compile sources and to generate the files 26 | # Using Java 17 is required since 2022.3 27 | javaVersion = 17 28 | 29 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 30 | org.gradle.configuration-cache = true 31 | 32 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 33 | org.gradle.caching = true 34 | -------------------------------------------------------------------------------- /tooling/lsp-clients/readme.md: -------------------------------------------------------------------------------- 1 | ## Language Server Protocol Clients 2 | 3 | This project contains clients of the [Language Server Protocol](../language-server-protocol). Currently, we have 4 | a [Monaco](./monaco) client and a [VS Code](./vscode) client. 5 | The monaco clients can be run with the following gradlew tasks: 6 | 7 | ```shell 8 | ./gradlew tooling:lsp-clients:npm_run_monaco 9 | ./gradlew tooling:lsp-clients:npm_run_vscode 10 | ``` 11 | 12 | ## Features 13 | 14 | - Syntax highlighting for `.kson` files 15 | - Language server integration 16 | - Auto-formatting 17 | - Diagnostics and error reporting 18 | 19 | ### Development 20 | 21 | The clients start a KSON Language Server. If we are on the browser runtime (Monaco or VS Code web) the server is started 22 | in a webworker. If we are on a node runtime the server is started in its own process. 23 | 24 | Since there is a distinction between node and browser runtimes within the VS Code client this client has both a `./node` 25 | and `./browser` directory for the client and server. The Monaco client only uses the browser runtime. 26 | 27 | `./shared` contains shared code for both clients. Currently, these are 28 | the [textmate grammar](./shared/extension/config/kson.tmLanguage.json) 29 | , [language configuration](./shared/extension/config/language-configuration.json), and the code 30 | to [start a server](./shared/src/connection/browserConnection.ts) for the browser runtime. 31 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/RefValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonValue 4 | import org.kson.parser.MessageSink 5 | import org.kson.schema.* 6 | 7 | /** 8 | * Validator for JSON Schema `$ref` references 9 | * 10 | * @param [resolvedRef] the [ResolvedRef] object for this $ref 11 | * @param [idLookup] the IdSchemaLookup for resolving nested $ref references within the referenced schema 12 | */ 13 | class RefValidator( 14 | private val resolvedRef: ResolvedRef, 15 | private val idLookup: SchemaIdLookup 16 | ) : JsonSchemaValidator { 17 | private val refSchema: JsonSchema? by lazy { 18 | // Parse the resolved value as a schema with the appropriate base URI context 19 | val messageSink = MessageSink() 20 | // TODO these parsed $ref schemas should be cached for efficiency 21 | SchemaParser.parseSchemaElement( 22 | resolvedRef.resolvedValue, 23 | messageSink, 24 | resolvedRef.resolvedValueBaseUri, 25 | idLookup) 26 | } 27 | 28 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 29 | val schema = refSchema 30 | ?: // Schema parsing failed, so can't perform validation against it 31 | return 32 | 33 | // Validate the value against our referenced schema 34 | schema.validate(ksonValue, messageSink) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tooling/lsp-clients/shared/src/connection/browserConnection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserMessageReader, 3 | BrowserMessageWriter, 4 | } from 'vscode-languageserver/browser'; 5 | import { createConnection } from 'vscode-languageserver/browser'; 6 | import { startKsonServer } from 'kson-language-server/browser'; 7 | 8 | /** 9 | * Creates browser-specific message reader and writer. 10 | * This is shared between VS Code Web and Monaco Editor. 11 | */ 12 | function createBrowserMessageTransports(context: Window & typeof globalThis) { 13 | const messageReader = new BrowserMessageReader(context); 14 | const messageWriter = new BrowserMessageWriter(context); 15 | 16 | return { messageReader, messageWriter }; 17 | } 18 | 19 | /** 20 | * Sets up console logging to use a connection. 21 | * Shared utility for redirecting console output to LSP connection. 22 | */ 23 | function setupConnectionLogging(connection: { console: { log: Function, error: Function } }) { 24 | console.log = connection.console.log.bind(connection.console); 25 | console.error = connection.console.error.bind(connection.console); 26 | } 27 | 28 | export function createAndStartBrowserWorker(){ 29 | 30 | const { messageReader, messageWriter } = createBrowserMessageTransports(self); 31 | const connection = createConnection(messageReader, messageWriter); 32 | 33 | setupConnectionLogging(connection); 34 | 35 | startKsonServer(connection); 36 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/parser/ParserTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.parser 2 | 3 | import org.kson.ast.AstNode 4 | import org.kson.CompileTarget 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertNotNull 8 | import kotlin.test.assertTrue 9 | 10 | /** 11 | * NOTE: most Kson parsing tests are done the more holistic level in [org.kson.KsonCoreTest]. If/when we have 12 | * a need to test specifically at the [Parser] class level, those tests belong here. 13 | */ 14 | class ParserTest { 15 | 16 | /** 17 | * NOTE: testing [Parser] directly rarely makes sense, and this is mostly a placeholder test. 18 | * Test in [org.kson.KsonCoreTest] instead. See class level doc above for more detail. 19 | */ 20 | @Test 21 | fun testSanityCheckParse() { 22 | val nullTokenStream = listOf( 23 | Token(TokenType.NULL, Lexeme("null", Location.create(0, 0, 0, 4, 0, 4)), "null"), 24 | Token(TokenType.EOF, Lexeme("", Location.create(0, 4, 0, 4, 4, 4)), "") 25 | ) 26 | val builder = KsonBuilder(nullTokenStream) 27 | Parser(builder).parse() 28 | val messageSink = MessageSink() 29 | val ksonRoot = builder.buildTree(messageSink) 30 | assertNotNull(ksonRoot) 31 | assertTrue(messageSink.loggedMessages().isEmpty()) 32 | assertEquals(ksonRoot.toSource(AstNode.Indent(), CompileTarget.Kson()), "null") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/editor/EmbedBlockLanguageListCompletionProvider.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.editor 2 | 3 | import com.intellij.codeInsight.completion.* 4 | import com.intellij.codeInsight.lookup.LookupElementBuilder 5 | import com.intellij.lang.Language 6 | import com.intellij.lang.LanguageUtil 7 | import com.intellij.openapi.util.text.StringUtil 8 | import com.intellij.ui.DeferredIconImpl 9 | import com.intellij.util.ProcessingContext 10 | import javax.swing.Icon 11 | 12 | internal class EmbedBlockLanguageListCompletionProvider : CompletionProvider() { 13 | override fun addCompletions( 14 | parameters: CompletionParameters, 15 | context: ProcessingContext, 16 | result: CompletionResultSet 17 | ) { 18 | for (language in LanguageUtil.getInjectableLanguages()) { 19 | val lookupElement = LookupElementBuilder.create(StringUtil.toLowerCase(language.id)) 20 | .withIcon(createLanguageIcon(language)) 21 | .withTypeText(language.displayName, true) 22 | result.addElement(lookupElement) 23 | } 24 | } 25 | 26 | companion object { 27 | fun createLanguageIcon(language: Language): Icon { 28 | return DeferredIconImpl(null, language, true) { curLanguage: Language -> 29 | curLanguage.associatedFileType?.icon 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/ConstValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.value.KsonValue 4 | import org.kson.parser.MessageSink 5 | import org.kson.parser.messages.MessageType 6 | import org.kson.schema.JsonSchemaValidator 7 | import org.kson.value.EmbedBlock 8 | import org.kson.value.KsonBoolean 9 | import org.kson.value.KsonList 10 | import org.kson.value.KsonNull 11 | import org.kson.value.KsonNumber 12 | import org.kson.value.KsonObject 13 | import org.kson.value.KsonString 14 | 15 | class ConstValidator(private val const: KsonValue) : JsonSchemaValidator { 16 | override fun validate(ksonValue: KsonValue, messageSink: MessageSink) { 17 | if (ksonValue != const) { 18 | val requiredValue = when (const) { 19 | is KsonNull -> "null" 20 | is KsonBoolean -> const.value.toString() 21 | is KsonNumber -> const.value.asString 22 | is KsonString -> const.value 23 | // TODO ksonValue should be able to serialize to its corresponding KSON 24 | is KsonObject -> "the schema-specified object" 25 | is KsonList -> "the schema-specified list" 26 | is EmbedBlock -> "the schema-specified embed block" 27 | } 28 | messageSink.error(ksonValue.location, MessageType.SCHEMA_VALUE_NOT_EQUAL_TO_CONST.create(requiredValue)) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/KsonCoreTestEmbedBlockError.kt: -------------------------------------------------------------------------------- 1 | package org.kson 2 | 3 | import org.kson.parser.messages.MessageType.* 4 | import kotlin.test.Test 5 | 6 | class KsonCoreTestEmbedBlockError : KsonCoreTestError { 7 | /** 8 | * Regression test for a parsing problem we had at the boundary: 9 | * this was blowing up with an index out of bounds rather than 10 | * parsing with an [EMBED_BLOCK_NO_CLOSE] error as it should 11 | */ 12 | @Test 13 | fun testUnclosedEmbedWithEscape() { 14 | assertParserRejectsSource( 15 | """ 16 | %% 17 | %\% 18 | """.trimIndent(), 19 | listOf(EMBED_BLOCK_NO_CLOSE) 20 | ) 21 | } 22 | 23 | @Test 24 | fun testUnclosedEmbedDelimiterError() { 25 | assertParserRejectsSource("%%\n", listOf(EMBED_BLOCK_NO_CLOSE)) 26 | } 27 | 28 | @Test 29 | fun testUnclosedEmbedAlternateDelimiterError() { 30 | assertParserRejectsSource("$\n", listOf(EMBED_BLOCK_NO_CLOSE)) 31 | } 32 | 33 | @Test 34 | fun testEmbedBlockPartialDelim() { 35 | assertParserRejectsSource( 36 | """ 37 | % 38 | """, 39 | listOf(EMBED_BLOCK_NO_CLOSE) 40 | ) 41 | 42 | assertParserRejectsSource( 43 | """ 44 | $ 45 | """, 46 | listOf(EMBED_BLOCK_NO_CLOSE) 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/CommandExecutor.browser.ts: -------------------------------------------------------------------------------- 1 | import {CommandExecutorBase} from './CommandExecutor.base.js'; 2 | import {CommandParameters} from './CommandParameters.js'; 3 | import {CommandType} from './CommandType.js'; 4 | 5 | /** 6 | * Browser implementation of CommandExecutor without file system support 7 | */ 8 | export class CommandExecutor extends CommandExecutorBase { 9 | /** 10 | * Execute the associate schema command (not supported in browser) 11 | */ 12 | protected async executeAssociateSchema(_commandArgs: CommandParameters[CommandType.ASSOCIATE_SCHEMA]): Promise { 13 | const errorMessage = 'Associate Schema command is not supported in browser environments (requires file system access)'; 14 | this.connection.window.showErrorMessage(errorMessage); 15 | 16 | return { 17 | success: false, 18 | message: errorMessage 19 | }; 20 | } 21 | 22 | /** 23 | * Execute the remove schema command (not supported in browser) 24 | */ 25 | protected async executeRemoveSchema(_commandArgs: CommandParameters[CommandType.REMOVE_SCHEMA]): Promise { 26 | const errorMessage = 'Remove Schema command is not supported in browser environments (requires file system access)'; 27 | this.connection.window.showErrorMessage(errorMessage); 28 | 29 | return { 30 | success: false, 31 | message: errorMessage 32 | }; 33 | } 34 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/KsonCoreTestStringError.kt: -------------------------------------------------------------------------------- 1 | package org.kson 2 | 3 | import org.kson.parser.messages.MessageType.* 4 | import kotlin.test.Test 5 | 6 | class KsonCoreTestStringError : KsonCoreTestError { 7 | @Test 8 | fun testBadStringEscape() { 9 | assertParserRejectsSource("'this has \\x which is an illegal escape'", listOf(STRING_BAD_ESCAPE)) 10 | } 11 | 12 | @Test 13 | fun testBadUnicodeEscape() { 14 | assertParserRejectsSource("'\\u12'", listOf(STRING_BAD_UNICODE_ESCAPE)) 15 | assertParserRejectsSource("'\\u12x9'", listOf(STRING_BAD_UNICODE_ESCAPE)) 16 | assertParserRejectsSource("'\\u'", listOf(STRING_BAD_UNICODE_ESCAPE)) 17 | assertParserRejectsSource("'\\u", listOf(STRING_NO_CLOSE, STRING_BAD_UNICODE_ESCAPE)) 18 | } 19 | 20 | @Test 21 | fun testDanglingEscapes() { 22 | assertParserRejectsSource("'\\'", listOf(STRING_NO_CLOSE)) 23 | assertParserRejectsSource("'\\", listOf(STRING_NO_CLOSE, STRING_BAD_ESCAPE)) 24 | } 25 | 26 | @Test 27 | fun testUnclosedStringError() { 28 | assertParserRejectsSource("\"unclosed", listOf(STRING_NO_CLOSE)) 29 | } 30 | 31 | @Test 32 | fun testTwoConsecutiveStrings() { 33 | assertParserRejectsSource("'a string''an illegal second string'", listOf(EOF_NOT_REACHED)) 34 | } 35 | 36 | @Test 37 | fun testStringWithNullByte() { 38 | assertParserRejectsSource("my_bad_string: 'aa' ", listOf(STRING_CONTROL_CHARACTER)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/README.md: -------------------------------------------------------------------------------- 1 | # Kson Language Server 2 | 3 | A Language Server Protocol (LSP) implementation for Kson, written in TypeScript. We support the 4 | [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/), because it is a standard that allows programming language tooling to be decoupled 5 | from the code editor. 6 | 7 | By implementing a language server, this project provides Kson language support that can be used by any LSP-compatible 8 | editor, such as Visual Studio Code, Neovim, or Sublime Text. This approach avoids the need to write a new extension for 9 | each editor and ensures that features are implemented in one place, improving performance and maintainability [1]. 10 | 11 | ## Current Features 12 | 13 | * **Real-time Diagnostics:** Identifies syntax errors as you type. 14 | * **Document Formatting:** Automatically formats Kson files. 15 | * **Semantic Highlighting:** Provides rich, context-aware syntax highlighting. 16 | 17 | ## Getting Started 18 | 19 | ### Prerequisites 20 | 21 | * Node.js (v20.0.0 or higher) 22 | 23 | ### Installation 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | ### Build 30 | 31 | To compile the TypeScript source code, run: 32 | 33 | ```bash 34 | npm run build 35 | ``` 36 | 37 | ### Testing 38 | 39 | To run the test suite: 40 | 41 | ```bash 42 | npm test 43 | ``` 44 | 45 | [1] Visual Studio Code. (2025). *Language Server Extension Guide*. Retrieved 46 | from https://code.visualstudio.com/api/language-extensions/language-server-extension-guide -------------------------------------------------------------------------------- /tooling/language-server-protocol/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kson-language-server", 3 | "authors": [ 4 | { 5 | "name": "Bart Dubbeldam", 6 | "email": "bart.dubbeldam@dc-analytic.com" 7 | }, 8 | { 9 | "name": "Daniel Marcotte", 10 | "email": "dmarcotte@gmail.com" 11 | } 12 | ], 13 | "version": "0.0.1", 14 | "description": "Kson Language Server implementation in TypeScript", 15 | "type": "module", 16 | "exports": { 17 | ".": { 18 | "types": "./out/index.d.ts", 19 | "default": "./out/index.js" 20 | }, 21 | "./node": { 22 | "types": "./out/index.node.d.ts", 23 | "default": "./out/index.node.js" 24 | }, 25 | "./browser": { 26 | "types": "./out/index.browser.d.ts", 27 | "default": "./out/index.browser.js" 28 | } 29 | }, 30 | "scripts": { 31 | "compile": "tsc", 32 | "test": "mocha 'out/test/**/*.test.js'" 33 | }, 34 | "keywords": [ 35 | "kson", 36 | "language-server", 37 | "lsp", 38 | "typescript" 39 | ], 40 | "engines": { 41 | "node": ">=20.0.0" 42 | }, 43 | "dependencies": { 44 | "kson": "file:../../kson-lib/build/dist/js/productionLibrary", 45 | "kson-tooling": "file:../../kson-tooling-lib/build/dist/js/productionLibrary", 46 | "vscode-languageserver": "^9.0.1", 47 | "vscode-languageserver-textdocument": "^1.0.12", 48 | "vscode-uri": "^3.0.8" 49 | }, 50 | "devDependencies": { 51 | "@types/mocha": "10.0.10", 52 | "@types/node": "20.0.0", 53 | "mocha": "^10.0.10", 54 | "tsx": "^4.20.2", 55 | "typescript": "^5.8.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/formatter/KsonExternalFormatter.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.formatter 2 | 3 | import com.intellij.formatting.FormattingContext 4 | import com.intellij.formatting.service.AbstractDocumentFormattingService 5 | import com.intellij.formatting.service.FormattingService.Feature 6 | import com.intellij.openapi.editor.Document 7 | import com.intellij.openapi.util.TextRange 8 | import com.intellij.psi.PsiFile 9 | import org.kson.jetbrains.psi.KsonPsiFile 10 | import org.kson.tools.IndentType 11 | import org.kson.tools.KsonFormatterConfig 12 | import org.kson.tools.format 13 | 14 | class KsonExternalFormatter : AbstractDocumentFormattingService() { 15 | override fun canFormat(file: PsiFile): Boolean { 16 | return file is KsonPsiFile 17 | } 18 | 19 | override fun getFeatures(): Set = emptySet() 20 | 21 | override fun formatDocument( 22 | document: Document, 23 | formattingRanges: MutableList, 24 | formattingContext: FormattingContext, 25 | canChangeWhiteSpaceOnly: Boolean, 26 | quickFormat: Boolean 27 | ) { 28 | val indentOptions = formattingContext.codeStyleSettings 29 | .getIndentOptions(formattingContext.containingFile.fileType) 30 | val indentType = if (indentOptions.USE_TAB_CHARACTER) { 31 | IndentType.Tab() 32 | } else { 33 | IndentType.Space(indentOptions.INDENT_SIZE) 34 | } 35 | 36 | val formatted = format(document.text, KsonFormatterConfig(indentType)) 37 | document.setText(formatted) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/psi/KsonElementGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.psi 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 4 | import org.kson.parser.behavior.embedblock.EmbedDelim 5 | 6 | class KsonElementGeneratorTest : BasePlatformTestCase() { 7 | private lateinit var generator: KsonElementGenerator 8 | 9 | override fun setUp() { 10 | super.setUp() 11 | generator = KsonElementGenerator(project) 12 | } 13 | 14 | fun testEmbedBlockGeneration() { 15 | val content = """ 16 | | content with a newline current indent of 2 17 | | 18 | """.trimMargin() 19 | val generatedBlock = generator.createEmbedBlock(EmbedDelim.Percent, content, tag = "custom") 20 | 21 | val expectedBlock = """ 22 | |%custom 23 | | content with a newline current indent of 2 24 | | %% 25 | """.trimMargin() 26 | assertEquals(generatedBlock.text, expectedBlock) 27 | } 28 | 29 | fun testEmbedBlockGenerationWithIndent() { 30 | val content = """ 31 | | content with a newline and added indent of 2 32 | | 33 | """.trimMargin() 34 | val generatedBlock = generator.createEmbedBlock(EmbedDelim.Percent, content, tag = "custom", indentText = " ") 35 | 36 | val expectedBlock = """ 37 | |%custom 38 | | content with a newline and added indent of 2 39 | | %% 40 | """.trimMargin() 41 | assertEquals(generatedBlock.text, expectedBlock) 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/examples/schemas/config.schema.kson: -------------------------------------------------------------------------------- 1 | '$schema': 'http://json-schema.org/draft-07/schema#' 2 | type: object 3 | title: 'Application Configuration' 4 | description: 'Configuration schema for the application' 5 | properties: 6 | appName: 7 | type: string 8 | description: 'The name of the application' 9 | title: 'Application Name' 10 | . 11 | version: 12 | type: string 13 | description: 'The version number' 14 | title: Version 15 | pattern: '^\\d+\\.\\d+\\.\\d+$' 16 | . 17 | server: 18 | type: object 19 | description: 'Server configuration' 20 | title: 'Server Settings' 21 | properties: 22 | host: 23 | type: string 24 | description: 'The server host address' 25 | title: Host 26 | default: localhost 27 | . 28 | port: 29 | type: number 30 | description: 'The port number the server listens on' 31 | title: 'Port Number' 32 | minimum: 1024 33 | maximum: 65535 34 | default: 8080 35 | . 36 | enabled: 37 | type: boolean 38 | description: 'Whether the server is enabled' 39 | title: 'Enabled Flag' 40 | default: true 41 | . 42 | . 43 | required: 44 | - host 45 | - port 46 | . 47 | features: 48 | type: array 49 | description: 'List of enabled features' 50 | title: Features 51 | items: 52 | type: string 53 | enum: 54 | - logging 55 | - metrics 56 | - tracing 57 | - caching 58 | . 59 | . 60 | . 61 | required: 62 | - appName 63 | - version -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import importMetaUrlPlugin from '@codingame/esbuild-import-meta-url-plugin'; 3 | import { resolve } from 'path'; 4 | 5 | export default defineConfig(({ }) => { 6 | return { 7 | base: './', 8 | optimizeDeps: { 9 | esbuildOptions: { 10 | plugins: [ 11 | importMetaUrlPlugin 12 | ] 13 | }, 14 | include: [ 15 | 'monaco-editor-wrapper', 16 | 'vscode-languageclient', 17 | 'vscode-textmate', 18 | '@codingame/monaco-vscode-textmate-service-override', 19 | 'vscode-oniguruma' 20 | ] 21 | }, 22 | worker: { 23 | format: 'es' 24 | }, 25 | build: { 26 | target: 'esnext', 27 | lib: { 28 | entry: resolve(__dirname, 'src/index.ts'), 29 | fileName: 'kson-monaco', 30 | formats: ['es'] 31 | }, 32 | rollupOptions: { 33 | output: { 34 | // Preserve named exports 35 | preserveModules: false, 36 | // Use a single chunk to ensure exports are accessible 37 | inlineDynamicImports: false 38 | } 39 | }, 40 | // Enable code splitting for better lazy loading 41 | minify: 'esbuild', 42 | sourcemap: true, 43 | }, 44 | server: { 45 | port: 5173, 46 | open: true 47 | } 48 | }; 49 | }); -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/CommandParameters.ts: -------------------------------------------------------------------------------- 1 | import { CommandType } from './CommandType.js'; 2 | import { Command } from 'vscode-languageserver'; 3 | import { FormattingStyle } from 'kson'; 4 | 5 | /** 6 | * Type-safe mapping of command types to their expected parameter structures 7 | */ 8 | export interface CommandParameters { 9 | [CommandType.PLAIN_FORMAT ]: { 10 | documentUri: string; 11 | formattingStyle: FormattingStyle; 12 | }; 13 | [CommandType.COMPACT_FORMAT ]: { 14 | documentUri: string; 15 | formattingStyle: FormattingStyle; 16 | }; 17 | [CommandType.DELIMITED_FORMAT]: { 18 | documentUri: string; 19 | formattingStyle: FormattingStyle; 20 | }; 21 | [CommandType.CLASSIC_FORMAT]: { 22 | documentUri: string; 23 | formattingStyle: FormattingStyle; 24 | }; 25 | [CommandType.ASSOCIATE_SCHEMA]: { 26 | documentUri: string; 27 | schemaPath: string; 28 | }; 29 | [CommandType.REMOVE_SCHEMA]: { 30 | documentUri: string; 31 | }; 32 | } 33 | 34 | /** 35 | * Type guard to check if a string is a valid CommandType 36 | */ 37 | export function isValidCommand(command: string): command is CommandType { 38 | return Object.values(CommandType).includes(command as CommandType); 39 | } 40 | 41 | /** 42 | * Helper function to create a type-safe command 43 | */ 44 | export function createTypedCommand( 45 | type: T, 46 | title: string, 47 | params: CommandParameters[T] 48 | ): Command { 49 | return { 50 | title, 51 | command: type, 52 | arguments: [params] 53 | }; 54 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/schema/validators/TypeValidator.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.parser.MessageSink 4 | import org.kson.parser.messages.MessageType 5 | import org.kson.schema.asSchemaInteger 6 | import org.kson.value.* 7 | 8 | /** 9 | * Json Schema `type:` validator 10 | */ 11 | class TypeValidator(private val allowedTypes: List) { 12 | constructor(type: String) : this(listOf(type)) 13 | 14 | /** 15 | * Validates whether the given [ksonValue] is valid according to this [TypeValidator]. 16 | * If [ksonValue] is invalid, validation errors are written to [messageSink] and this method returns false 17 | * 18 | * @return true if [ksonValue] is valid, [false otherwise] 19 | */ 20 | fun validate(ksonValue: KsonValue, messageSink: MessageSink): Boolean { 21 | val nodeType = when (ksonValue) { 22 | is KsonBoolean -> "boolean" 23 | is KsonNull -> "null" 24 | is KsonNumber -> { 25 | if (asSchemaInteger(ksonValue) != null) { 26 | "integer" 27 | } else { 28 | "number" 29 | } 30 | } 31 | is KsonString -> "string" 32 | is KsonList -> "array" 33 | is KsonObject -> "object" 34 | is EmbedBlock -> "object" 35 | } 36 | 37 | if (!allowedTypes.contains(nodeType) 38 | // if our node is an integer, this type is valid if the more-general "number" is an allowedType 39 | && !(nodeType == "integer" && allowedTypes.contains("number"))) { 40 | messageSink.error(ksonValue.location, MessageType.SCHEMA_VALUE_TYPE_MISMATCH.create(allowedTypes.joinToString(), nodeType)) 41 | return false 42 | } 43 | 44 | return true 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/inject/KsonLanguageInjectionContributor.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.inject 2 | 3 | import com.intellij.lang.Language 4 | import com.intellij.lang.LanguageUtil 5 | import com.intellij.lang.injection.general.Injection 6 | import com.intellij.lang.injection.general.LanguageInjectionContributor 7 | import com.intellij.lang.injection.general.SimpleInjection 8 | import com.intellij.psi.PsiElement 9 | import org.kson.jetbrains.psi.KsonEmbedContent 10 | 11 | /** 12 | * Language injection contributor for KSON embed content blocks. 13 | * In this class an injection is 'prepared' that is performed by the [KsonLanguageInjectionPerformer] 14 | */ 15 | class KsonLanguageInjectionContributor : LanguageInjectionContributor { 16 | 17 | override fun getInjection(context: PsiElement): Injection? { 18 | // Only inject into KsonEmbedContent elements 19 | if (context !is KsonEmbedContent || !context.isValidHost) { 20 | return null 21 | } 22 | 23 | val embedBlock = context.embedBlock ?: return null 24 | val languageTag = embedBlock.embedBlockTag ?: return null 25 | 26 | // Find the language by tag 27 | val language = findLanguageForInjection(languageTag) ?: return null 28 | 29 | return SimpleInjection(language, "", "", null) 30 | } 31 | 32 | private fun findLanguageForInjection(languageTag: String): Language? { 33 | val registeredLanguages = Language.getRegisteredLanguages() 34 | val language = registeredLanguages.find { it.id.equals(languageTag, ignoreCase = true) } 35 | return language?.takeIf { LanguageUtil.isInjectableLanguage(it) } 36 | } 37 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/inject/KsonLanguageInjectionPerformer.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.inject 2 | 3 | import com.intellij.lang.injection.general.Injection 4 | import com.intellij.lang.injection.general.LanguageInjectionPerformer 5 | import com.intellij.lang.injection.MultiHostRegistrar 6 | import com.intellij.psi.PsiElement 7 | import org.kson.jetbrains.psi.KsonEmbedContent 8 | 9 | /** 10 | * Language injection performer for KSON embed content blocks. 11 | * Handles the actual injection mechanics using the existing robust infrastructure. 12 | */ 13 | class KsonLanguageInjectionPerformer : LanguageInjectionPerformer { 14 | override fun isPrimary(): Boolean { 15 | return true 16 | } 17 | 18 | override fun performInjection(registrar: MultiHostRegistrar, injection: Injection, context: PsiElement): Boolean { 19 | if (context !is KsonEmbedContent || !context.isValidHost) { 20 | return false 21 | } 22 | 23 | // Get the ranges that will be used for injection using existing infrastructure 24 | val ranges = context.indentHandler.getUntrimmedRanges(context) 25 | if (ranges.isEmpty()) return false 26 | 27 | val language = injection.injectedLanguage ?: return false 28 | registrar.startInjecting(language) 29 | 30 | // Process each range individually to maintain proper mapping 31 | for (range in ranges) { 32 | registrar.addPlace( 33 | injection.prefix, 34 | injection.suffix, 35 | context, 36 | range 37 | ) 38 | } 39 | 40 | registrar.doneInjecting() 41 | return true 42 | } 43 | } -------------------------------------------------------------------------------- /tooling/jetbrains/readme.md: -------------------------------------------------------------------------------- 1 | # [Kson](https://kson.org) support for [JetBrains IDEs](https://www.jetbrains.com/products/#type=ide) 2 | 3 | This [Gradle subproject](https://docs.gradle.org/current/userguide/multi_project_builds.html) implements [Kson](https://kson.org) support for [JetBrains IDEs](https://www.jetbrains.com/products/#type=ide) 4 | 5 | ### Development 6 | 7 | This is a subproject of the main [Kson](../../readme.md) implementation, and so requires no special setup. 8 | 9 | **NOTE: all subproject Gradle commands are run from the [project root](../..)**. 10 | 11 | Some useful Gradle commands for this subproject: 12 | 13 | ```bash 14 | # Run Intellij IDEA with the development plugin installed 15 | ./gradlew :tooling:jetbrains:runIde 16 | 17 | # Run the plugin tests 18 | ./gradlew :tooling:jetbrains:test 19 | 20 | # Verify plugin compatibility 21 | # (https://plugins.jetbrains.com/docs/intellij/api-changes-list.html#verifying-compatibility) 22 | ./gradlew :tooling:jetbrains:runPluginVerifier 23 | ``` 24 | 25 | ### Intellij Platform Upgrades and Maintenance 26 | 27 | This (sub)project is built from a stripped-down version of the [Intellij Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template), so when upgrading to integrate with newer versions of the Intellij Platform, we should consult that project for updates and documentation on the latest plugin integration/API guidance 28 | 29 | ### Running and Debugging 30 | 31 | Once your [Kson](../../readme.md) project is loaded into intellij as described in [**Development setup**](https://github.com/kson-org/kson#development-setup), the `tooling:jetbrains:runIde` Gradle task can be run and debugged from the Gradle pane within Intellij 32 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/suite/syntax-highlighting.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { assert } from './assert'; 3 | import { createTestFile, cleanUp } from './common'; 4 | import TextmateLanguageService from 'vscode-textmate-languageservice'; 5 | 6 | describe('Syntax Highlighting Tests', () => { 7 | let testFileUri: vscode.Uri | undefined; 8 | 9 | afterEach(async () => { 10 | if (testFileUri) { 11 | await cleanUp(testFileUri); 12 | testFileUri = undefined; 13 | } 14 | }); 15 | 16 | async function getTokenScopesAtPosition(document: vscode.TextDocument, line: number, character: number): Promise { 17 | const position = new vscode.Position(line, character); 18 | const tokenInfo = await TextmateLanguageService.api.getScopeInformationAtPosition(document, position); 19 | console.log(`Token at line ${line}, char ${character}:`, tokenInfo); 20 | return tokenInfo.scopes; 21 | } 22 | 23 | it('Should highlight Python embedded blocks', async () => { 24 | const content = `key: %python 25 | print("Hello, World!") 26 | def greet(name): 27 | return f"Hello, {name}!" 28 | %%`; 29 | 30 | const [uri, document] = await createTestFile(content); 31 | testFileUri = uri; 32 | 33 | // Check that Python code is tagged as python 34 | const pythonScopes = await getTokenScopesAtPosition(document, 1, 10); 35 | console.log("Python code scopes:", pythonScopes); 36 | assert.ok(pythonScopes.some(scope => scope.includes('source.python') || scope.includes('meta.embedded.python'))); 37 | }).timeout(10000); 38 | }); -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/parser/behavior/embedblock/EmbedBlockSerialization.kt: -------------------------------------------------------------------------------- 1 | package org.kson.parser.behavior.embedblock 2 | 3 | import org.kson.ast.StringNodeImpl 4 | import org.kson.ast.EmbedBlockNode 5 | 6 | /** 7 | * An Embed Block is equivalent to an object with a string property named [EmbedObjectKeys.EMBED_CONTENT] and NO 8 | * other properties except (optionally) [EmbedObjectKeys.EMBED_TAG] or [EmbedObjectKeys.EMBED_METADATA]. Objects 9 | * of this shape can be serialized to and from our Embed Block syntax without any loss or corruption of the data. 10 | * The embed block syntax may be considered a "view" on objects of this shape. 11 | */ 12 | enum class EmbedObjectKeys(val key: String) { 13 | EMBED_TAG("embedTag"), 14 | EMBED_METADATA("embedMetadata"), 15 | EMBED_CONTENT("embedContent"); 16 | 17 | companion object { 18 | /** 19 | * Check whether the given [properties] can be decoded to an [EmbedBlockNode]. 20 | * This is the case when: 21 | * 1. All values of [properties] are [StringNodeImpl]'s. 22 | * 2. [properties] contains an [EMBED_CONTENT] key 23 | * 3. [properties] contains no other keys than [EmbedObjectKeys.entries] 24 | */ 25 | fun canBeDecoded(properties: Map): Boolean{ 26 | val allStrings = properties.all { it.value != null } 27 | val containsContent = properties.containsKey(EMBED_CONTENT.key) 28 | val hasOnlyEmbedObjectKeys = properties.keys.all { key -> 29 | EmbedObjectKeys.entries.any { it.key == key } 30 | } 31 | return !(!allStrings || !containsContent || !hasOnlyEmbedObjectKeys) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/index.common.ts: -------------------------------------------------------------------------------- 1 | // Common test runner logic shared between Node and browser environments 2 | import testManifest from './test-files.json'; 3 | 4 | export async function runTests(mocha: any, isBrowser: boolean): Promise { 5 | return new Promise((resolve, reject) => { 6 | // Load tests 7 | const loadTests = async () => { 8 | if (isBrowser) { 9 | // Browser: use dynamic imports from manifest 10 | for (const testName of testManifest.tests) { 11 | await import(`./suite/${testName}.ts`); 12 | } 13 | } else { 14 | // Node: use addFile 15 | const path = await import('path'); 16 | const testsRoot = path.resolve(__dirname); 17 | 18 | // Use test files from manifest 19 | testManifest.tests.forEach(testName => { 20 | mocha.addFile(path.resolve(testsRoot, 'suite', `${testName}.js`)); 21 | }); 22 | } 23 | }; 24 | 25 | // Run tests 26 | loadTests().then(() => { 27 | try { 28 | mocha.run((failures: number) => { 29 | if (failures > 0) { 30 | reject(new Error(`${failures} tests failed.`)); 31 | } else { 32 | resolve(); 33 | } 34 | }); 35 | } catch (err) { 36 | console.error(err); 37 | reject(err); 38 | } 39 | }).catch(err => { 40 | console.error('Failed to load tests:', err); 41 | reject(err); 42 | }); 43 | }); 44 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | base 3 | } 4 | 5 | tasks { 6 | val npmInstall = register("npmInstall") { 7 | command=listOf("npm", "install") 8 | doNotTrackState("npm already tracks its own state") 9 | dependsOn(":tooling:language-server-protocol:npm_run_test") 10 | } 11 | 12 | register("npm_run_vscode") { 13 | command=listOf("npm", "run", "vscode") 14 | dependsOn(npmInstall) 15 | } 16 | 17 | register("npm_run_monaco") { 18 | command=listOf("npm", "run", "monaco") 19 | dependsOn(npmInstall) 20 | } 21 | 22 | val test = register("npm_run_test") { 23 | command=listOf("npm", "run", "test") 24 | dependsOn(npmInstall) 25 | } 26 | 27 | val buildVsCode = register("npm_run_buildVSCode") { 28 | command=listOf("npm", "run", "buildVSCode") 29 | dependsOn(npmInstall) 30 | } 31 | 32 | val buildMonaco = register("npm_run_buildMonaco") { 33 | command=listOf("npm", "run", "buildMonaco") 34 | dependsOn(npmInstall) 35 | } 36 | 37 | check { 38 | dependsOn(test) 39 | /** 40 | * TODO - Ideally this task is "npm_run_buildPlugins" building both plugins, however, for now the Monaco Vite build 41 | * is too unpredictable in CI 42 | */ 43 | dependsOn(buildVsCode) 44 | } 45 | 46 | clean { 47 | delete("node_modules") 48 | delete("vscode/out") 49 | delete("vscode/dist") 50 | delete("vscode/node_modules") 51 | delete("monaco/dist") 52 | delete("monaco/node_modules") 53 | delete("shared/out") 54 | } 55 | } -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/features/HoverService.ts: -------------------------------------------------------------------------------- 1 | import {Hover, Position, MarkupKind} from 'vscode-languageserver'; 2 | import {KsonDocument} from '../document/KsonDocument.js'; 3 | import {KsonTooling} from 'kson-tooling'; 4 | 5 | /** 6 | * Service for providing hover information based on JSON Schema. 7 | */ 8 | export class HoverService { 9 | 10 | /** 11 | * Get hover information for a position in a document. 12 | * 13 | * @param document The document to get hover info for 14 | * @param position The position in the document 15 | * @returns Hover information, or null if none available 16 | */ 17 | getHover(document: KsonDocument, position: Position): Hover | null { 18 | // Get the schema for this document 19 | const schemaDocument = document.getSchemaDocument(); 20 | if (!schemaDocument) { 21 | // No schema configured, no hover info available 22 | return null; 23 | } 24 | 25 | // Call the KsonTooling API to get schema hover info at this position 26 | // The API now accepts line and column directly, avoiding the Location mangling issue 27 | const tooling = KsonTooling.getInstance(); 28 | const hoverMarkdown = tooling.getSchemaInfoAtLocation( 29 | document.getText(), 30 | schemaDocument.getText(), 31 | position.line, 32 | position.character 33 | ); 34 | 35 | if (!hoverMarkdown) { 36 | return null; 37 | } 38 | 39 | // Return LSP Hover response with markdown content 40 | return { 41 | contents: { 42 | kind: MarkupKind.Markdown, 43 | value: hoverMarkdown 44 | } 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/schema/SchemaConfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for mapping KSON files to their JSON schemas. 3 | */ 4 | 5 | /** 6 | * A single schema mapping that associates file patterns with a schema. 7 | */ 8 | export interface SchemaMapping { 9 | /** 10 | * Glob patterns to match file paths. 11 | * Examples: ["config/*.kson"] or glob patterns with wildcards 12 | */ 13 | fileMatch: string[]; 14 | 15 | /** 16 | * Workspace-relative path to the schema file. 17 | * Example: "schemas/config.schema.json" 18 | */ 19 | schema: string; 20 | } 21 | 22 | /** 23 | * The root configuration object for .kson-schema.json 24 | */ 25 | export interface SchemaConfig { 26 | /** 27 | * Array of schema mappings. 28 | */ 29 | schemas: SchemaMapping[]; 30 | } 31 | 32 | /** 33 | * Type guard to check if an object is a valid SchemaConfig 34 | */ 35 | export function isValidSchemaConfig(obj: unknown): obj is SchemaConfig { 36 | if (typeof obj !== 'object' || obj === null) { 37 | return false; 38 | } 39 | 40 | const config = obj as Record; 41 | 42 | if (!Array.isArray(config.schemas)) { 43 | return false; 44 | } 45 | 46 | return config.schemas.every((mapping: unknown) => { 47 | if (typeof mapping !== 'object' || mapping === null) { 48 | return false; 49 | } 50 | 51 | const m = mapping as Record; 52 | 53 | return ( 54 | Array.isArray(m.fileMatch) && 55 | m.fileMatch.every((pattern: unknown) => typeof pattern === 'string') && 56 | typeof m.schema === 'string' 57 | ); 58 | }); 59 | } 60 | 61 | export const SCHEMA_CONFIG_FILENAME = '.kson-schema.kson'; 62 | -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/parser/KsonLexerTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.parser 2 | 3 | import com.intellij.testFramework.LightPlatformTestCase 4 | import org.kson.parser.TokenType 5 | 6 | class KsonLexerTest : LightPlatformTestCase() { 7 | 8 | fun testLexerWithNonZeroStartOffset() { 9 | val fullBuffer = """prefix content {key:value}""" 10 | val lexer = KsonLexer() 11 | 12 | // Start lexing from offset 15 (after "prefix content ") 13 | val startOffset = 15 14 | val endOffset = fullBuffer.length 15 | lexer.start(fullBuffer, startOffset, endOffset, 0) 16 | 17 | // { 18 | assertEquals(elem(TokenType.CURLY_BRACE_L), lexer.tokenType) 19 | // Token start should be at position 15 (not 0) 20 | assertEquals(15, lexer.tokenStart) 21 | assertEquals(16, lexer.tokenEnd) 22 | 23 | // key 24 | lexer.advance() 25 | assertEquals(elem(TokenType.UNQUOTED_STRING), lexer.tokenType) 26 | // Token positions should be relative to the full buffer 27 | assertEquals(16, lexer.tokenStart) 28 | assertEquals(19, lexer.tokenEnd) 29 | 30 | // : 31 | lexer.advance() 32 | assertEquals(elem(TokenType.COLON), lexer.tokenType) 33 | assertEquals(19, lexer.tokenStart) 34 | assertEquals(20, lexer.tokenEnd) 35 | 36 | // value 37 | lexer.advance() 38 | assertEquals(elem(TokenType.UNQUOTED_STRING), lexer.tokenType) 39 | assertEquals(20, lexer.tokenStart) 40 | assertEquals(25, lexer.tokenEnd) 41 | 42 | // } 43 | lexer.advance() 44 | assertEquals(elem(TokenType.CURLY_BRACE_R), lexer.tokenType) 45 | assertEquals(25, lexer.tokenStart) 46 | assertEquals(26, lexer.tokenEnd) 47 | } 48 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/suite/common.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { v4 as uuid } from 'uuid'; 3 | import { assert } from './assert'; 4 | 5 | export async function createTestFile(initialContent: string = '', fileName: string = `${uuid()}.kson`): Promise<[vscode.Uri, vscode.TextDocument]> { 6 | const workspaceFolder = getWorkspaceFolder(); 7 | 8 | // Always use VS Code's URI joining which works in both environments 9 | const uri = vscode.Uri.joinPath(workspaceFolder.uri, fileName); 10 | 11 | // Use TextEncoder for browser compatibility (works in Node.js too) 12 | const encoder = new TextEncoder(); 13 | await vscode.workspace.fs.writeFile(uri, encoder.encode(initialContent)); 14 | 15 | const document = await vscode.workspace.openTextDocument(uri); 16 | await vscode.window.showTextDocument(document); 17 | 18 | return [uri, document]; 19 | } 20 | 21 | export async function cleanUp(uri: vscode.Uri) { 22 | if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.uri.toString() === uri.toString()) { 23 | await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); 24 | } 25 | await vscode.workspace.fs.delete(uri, { useTrash: false }); 26 | } 27 | 28 | export function assertTextEqual(document: vscode.TextDocument, expectedText: string) { 29 | // Note: we transform `\r\n` into `\n` for cross-platform compatibility 30 | const actualText = document.getText().replace(/\r\n/g, '\n'); 31 | assert.strictEqual(actualText, expectedText); 32 | } 33 | 34 | function getWorkspaceFolder(): vscode.WorkspaceFolder { 35 | const workspaceFolders = vscode.workspace.workspaceFolders; 36 | if (!workspaceFolders || workspaceFolders.length === 0) { 37 | throw new Error('No workspace folder open'); 38 | } 39 | return workspaceFolders[0]; 40 | } -------------------------------------------------------------------------------- /jdk.properties: -------------------------------------------------------------------------------- 1 | ## 2 | # Declare the desired JDK version for our build. We use the `me.filippov.gradle.jvm.wrapper` Gradle plugin 3 | # to include this JDK in our Gradle wrapper, making this our truly "built-in" JDK for a trivial and robust 4 | # developer setup 5 | JDK_VERSION=21.0.2 6 | 7 | ## 8 | # Cross-platform download URLs for the version of the JDK we are currently using, used by the 9 | # `me.filippov.gradle.jvm.wrapper` Gradle plugin 10 | # 11 | # Source: https://adoptium.net/temurin/releases/ 12 | # 13 | # The JDK specified here is installed automatically on invocations of `./gradlew` 14 | # and is used by Gradle itself to run, which then uses it as the default JDK `toolchain` 15 | # for the entire build. 16 | # 17 | # NOTE: these URLs must point to a JDK that matches the declared JDK_VERSION above, else 18 | # the BAD_JDK_VERSION_ERROR will be triggered 19 | # 20 | # IF THESE URLs CHANGE: `./gradlew wrapper` must be run after any edits for them to take effect, 21 | # and `./gradlew check` should also be run to ensure the new JDK is loaded and verified 22 | unixJvmInstallDir=$APP_HOME/gradle/jdk 23 | winJvmInstallDir=%APP_HOME%\\gradle\\jdk 24 | macAarch64JvmUrl=https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_macos-aarch64_bin.tar.gz 25 | macX64JvmUrl=https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_macos-x64_bin.tar.gz 26 | linuxAarch64JvmUrl = https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-aarch64_bin.tar.gz 27 | linuxX64JvmUrl = https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz 28 | windowsX64JvmUrl = https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_windows-x64_bin.zip 29 | 30 | -------------------------------------------------------------------------------- /tooling/lsp-clients/vscode/test/suite/editing.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { createTestFile, cleanUp, assertTextEqual } from './common'; 3 | 4 | 5 | describe('Editing Tests', () => { 6 | let testFileUri: vscode.Uri | undefined; 7 | 8 | afterEach(async () => { 9 | if (testFileUri) { 10 | await cleanUp(testFileUri); 11 | testFileUri = undefined; 12 | } 13 | }); 14 | 15 | it('Should auto-close $ to $$$', async () => { 16 | const [uri, document] = await createTestFile(); 17 | testFileUri = uri; 18 | 19 | await vscode.commands.executeCommand('type', { text: '$' }); 20 | 21 | assertTextEqual(document, '$$$'); 22 | }).timeout(10000); 23 | 24 | it('Should auto-close % to %%%', async () => { 25 | const [uri, document] = await createTestFile(); 26 | testFileUri = uri; 27 | 28 | await vscode.commands.executeCommand('type', { text: '%' }); 29 | 30 | assertTextEqual(document, '%%%'); 31 | }).timeout(10000); 32 | 33 | it('Should indent an embed block delimited with $, without tag', async () => { 34 | const [uri, document] = await createTestFile(); 35 | testFileUri = uri; 36 | 37 | await vscode.commands.executeCommand('type', { text: 'key: $\n' }); 38 | 39 | assertTextEqual(document, [ 40 | 'key: $', 41 | ' $$' 42 | ].join('\n')); 43 | }).timeout(10000); 44 | 45 | it('Should indent an embed block delimited with %, with tag', async () => { 46 | const [uri, document] = await createTestFile(); 47 | testFileUri = uri; 48 | 49 | await vscode.commands.executeCommand('type', { text: 'key: %tag\n' }); 50 | 51 | assertTextEqual(document, [ 52 | 'key: %tag', 53 | ' %%' 54 | ].join('\n')); 55 | }).timeout(10000); 56 | }); -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/KsonCoreTestNumber.kt: -------------------------------------------------------------------------------- 1 | package org.kson 2 | 3 | import kotlin.test.Test 4 | 5 | class KsonCoreTestNumber : KsonCoreTest { 6 | /** 7 | * See also [org.kson.parser.NumberParserTest] for more targeted number parsing tests 8 | */ 9 | @Test 10 | fun testNumberLiteralSource() { 11 | assertParsesTo("42", "42", "42", "42") 12 | assertParsesTo("042", "42", "42", "42") 13 | assertParsesTo("42.1", "42.1", "42.1", "42.1") 14 | assertParsesTo("00042.1", "42.1", "42.1", "42.1") 15 | assertParsesTo("42.1E0", "42.1e0", "42.1e0", "42.1e0") 16 | assertParsesTo("42.1e0", "42.1e0", "42.1e0", "42.1e0") 17 | assertParsesTo("4.21E1", "4.21e1", "4.21e1", "4.21e1") 18 | assertParsesTo("421E-1", "421e-1", "421e-1", "421e-1") 19 | assertParsesTo("4210e-2", "4210e-2", "4210e-2", "4210e-2") 20 | assertParsesTo("0.421e2", "0.421e2", "0.421e2", "0.421e2") 21 | assertParsesTo("0.421e+2", "0.421e+2", "0.421e+2", "0.421e+2") 22 | assertParsesTo("42.1E+0", "42.1e+0", "42.1e+0", "42.1e+0") 23 | assertParsesTo("00042.1E0", "42.1e0", "42.1e0", "42.1e0") 24 | assertParsesTo("-42.1", "-42.1", "-42.1", "-42.1") 25 | assertParsesTo("-42.1E0", "-42.1e0", "-42.1e0", "-42.1e0") 26 | assertParsesTo("-42.1e0", "-42.1e0", "-42.1e0", "-42.1e0") 27 | assertParsesTo("-4.21E1", "-4.21e1", "-4.21e1", "-4.21e1") 28 | assertParsesTo("-421E-1", "-421e-1", "-421e-1", "-421e-1") 29 | assertParsesTo("-4210e-2", "-4210e-2", "-4210e-2", "-4210e-2") 30 | assertParsesTo("-0.421e2", "-0.421e2", "-0.421e2", "-0.421e2") 31 | assertParsesTo("-0.421e+2", "-0.421e+2", "-0.421e+2", "-0.421e+2") 32 | assertParsesTo("-42.1E+0", "-42.1e+0", "-42.1e+0", "-42.1e+0") 33 | assertParsesTo("-00042.1E0", "-42.1e0", "-42.1e0", "-42.1e0") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib-python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "kson-lang" 3 | # [[kson-version-num]] 4 | version = "0.3.0.dev0" 5 | authors = [ 6 | { name="KSON", email="kson@kson.org" }, 7 | ] 8 | description = "KSON: a next-gen configuration language and love letter to the humans maintaining computer configuration" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "cffi>=1.17.1", 13 | ] 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | "Topic :: File Formats :: JSON", 19 | "Topic :: File Formats :: JSON :: JSON Schema", 20 | "Topic :: Text Processing :: Markup", 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13", 26 | "Programming Language :: Python :: Implementation :: CPython", 27 | "Operating System :: MacOS :: MacOS X", 28 | "Operating System :: Microsoft :: Windows", 29 | "Operating System :: POSIX :: Linux", 30 | ] 31 | license = "Apache-2.0" 32 | license-files = ["LICENSE*"] 33 | 34 | [project.urls] 35 | Homepage = "https://github.com/kson-org/kson" 36 | Issues = "https://github.com/kson-org/kson/issues" 37 | 38 | 39 | [build-system] 40 | requires = ["setuptools>=61.0", "wheel"] 41 | build-backend = "build_backend" 42 | backend-path = ["."] 43 | 44 | [tool.setuptools] 45 | packages = ["kson"] 46 | package-dir = {"" = "src"} 47 | 48 | [tool.setuptools.package-data] 49 | kson = ["kson_api.h", "kson.dll", "libkson.dylib", "libkson.so"] 50 | 51 | [dependency-groups] 52 | dev = [ 53 | "pyright>=1.1.403", 54 | "pytest>=8.4.1", 55 | "ruff>=0.12.8", 56 | "cibuildwheel>=2.16.0", 57 | "twine>=6.2.0", 58 | ] 59 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/CommandExecutor.node.ts: -------------------------------------------------------------------------------- 1 | import {CommandExecutorBase} from './CommandExecutor.base.js'; 2 | import {CommandParameters} from './CommandParameters.js'; 3 | import {CommandType} from './CommandType.js'; 4 | import {AssociateSchemaCommand} from './AssociateSchemaCommand.js'; 5 | import {RemoveSchemaCommand} from './RemoveSchemaCommand.js'; 6 | 7 | /** 8 | * Node.js implementation of CommandExecutor with file system support 9 | */ 10 | export class CommandExecutor extends CommandExecutorBase { 11 | /** 12 | * Execute the associate schema command (Node.js only - requires file system access) 13 | */ 14 | protected async executeAssociateSchema(commandArgs: CommandParameters[CommandType.ASSOCIATE_SCHEMA]): Promise { 15 | const result = AssociateSchemaCommand.execute({ 16 | documentUri: commandArgs.documentUri, 17 | schemaPath: commandArgs.schemaPath, 18 | workspaceRoot: this.workspaceRoot 19 | }); 20 | 21 | if (result.success) { 22 | this.connection.window.showInformationMessage(result.message); 23 | } else { 24 | this.connection.window.showErrorMessage(result.message); 25 | } 26 | 27 | return result; 28 | } 29 | 30 | /** 31 | * Execute the remove schema command (Node.js only - requires file system access) 32 | */ 33 | protected async executeRemoveSchema(commandArgs: CommandParameters[CommandType.REMOVE_SCHEMA]): Promise { 34 | const result = RemoveSchemaCommand.execute({ 35 | documentUri: commandArgs.documentUri, 36 | workspaceRoot: this.workspaceRoot 37 | }); 38 | 39 | if (result.success) { 40 | this.connection.window.showInformationMessage(result.message); 41 | } else { 42 | this.connection.window.showErrorMessage(result.message); 43 | } 44 | 45 | return result; 46 | } 47 | } -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | KSON Editor 6 | 7 | 8 | 17 | 18 | 19 | 20 |
21 |

KSON Monaco Editor

22 |
23 | 24 |
25 | 26 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/AssociateSchemaCommand.ts: -------------------------------------------------------------------------------- 1 | import { SchemaConfigManager } from './SchemaConfigManager.js'; 2 | 3 | export interface AssociateSchemaParams { 4 | documentUri: string; 5 | schemaPath: string; 6 | workspaceRoot: string | null; 7 | } 8 | 9 | export interface AssociateSchemaResult { 10 | success: boolean; 11 | message: string; 12 | } 13 | 14 | /** 15 | * Associates a schema with a document by updating the .kson-schema.kson configuration file. 16 | * 17 | * This command: 18 | * 1. Loads or creates the .kson-schema.kson file 19 | * 2. Adds or updates a schema mapping for the document 20 | * 3. Uses exact file path matching (no wildcards) for precise association 21 | * 4. Saves the updated configuration back to disk 22 | */ 23 | export class AssociateSchemaCommand { 24 | /** 25 | * Execute the associate schema command. 26 | * 27 | * @param params Parameters containing document URI, schema path, and workspace root 28 | * @returns Result indicating success or failure with a message 29 | */ 30 | static execute(params: AssociateSchemaParams): AssociateSchemaResult { 31 | const {documentUri, schemaPath, workspaceRoot} = params; 32 | 33 | if (!workspaceRoot) { 34 | return { 35 | success: false, 36 | message: 'No workspace root available. Cannot create schema configuration.' 37 | }; 38 | } 39 | 40 | try { 41 | const configManager = SchemaConfigManager.load(workspaceRoot); 42 | configManager.associateSchema(documentUri, schemaPath); 43 | configManager.save(); 44 | 45 | return { 46 | success: true, 47 | message: `Associated schema "${schemaPath}" with document` 48 | }; 49 | } catch (error) { 50 | return { 51 | success: false, 52 | message: `Failed to associate schema: ${error}` 53 | }; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tooling/lsp-clients/monaco/src/KsonEditorManager.ts: -------------------------------------------------------------------------------- 1 | import { MonacoEditorLanguageClientWrapper, WrapperConfig } from 'monaco-editor-wrapper'; 2 | import { createKsonLanguageConfig } from './config/ksonConfig.js'; 3 | import workerUrl from './worker/ksonServer?worker&url'; 4 | 5 | 6 | /** 7 | * Creates a Kson-specific WrapperConfig with sensible defaults. 8 | * You can override any properties by spreading your own config. 9 | */ 10 | async function createKsonConfig(overrides: Partial = {}): Promise { 11 | // Create a dedicated worker for this editor 12 | const worker = new Worker(workerUrl, { 13 | type: 'module', 14 | name: `Kson LS (${Math.random().toString(36).substring(2, 15)})`, 15 | }); 16 | 17 | const baseConfig = createKsonLanguageConfig({ 18 | worker 19 | }); 20 | 21 | // Deep merge base config with overrides 22 | const finalConfig: WrapperConfig = { 23 | ...baseConfig, 24 | ...overrides, 25 | vscodeApiConfig: { 26 | ...baseConfig.vscodeApiConfig, 27 | ...overrides.vscodeApiConfig 28 | }, 29 | languageClientConfigs: { 30 | ...baseConfig.languageClientConfigs, 31 | ...overrides.languageClientConfigs 32 | }, 33 | editorAppConfig: { 34 | ...baseConfig.editorAppConfig, 35 | ...overrides.editorAppConfig, 36 | } 37 | }; 38 | 39 | // Attach worker reference for cleanup 40 | (finalConfig as any).__worker = worker; 41 | 42 | return finalConfig; 43 | } 44 | 45 | /** 46 | * Creates a Kson editor wrapper using the standard monaco-editor-wrapper. 47 | * Returns the wrapper which can be managed using its native API. 48 | */ 49 | export async function createKsonEditor(config: Partial = {}): Promise { 50 | const ksonConfig = await createKsonConfig(config); 51 | 52 | const wrapper = new MonacoEditorLanguageClientWrapper(); 53 | await wrapper.init(ksonConfig); 54 | 55 | return wrapper; 56 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/kotlin/org/kson/jetbrains/KsonPsiFileTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains 2 | 3 | import com.intellij.testFramework.fixtures.BasePlatformTestCase 4 | import com.intellij.util.PsiErrorElementUtil 5 | import org.kson.jetbrains.file.KsonFileType 6 | import org.kson.jetbrains.psi.KsonPsiFile 7 | 8 | class KsonPsiFileTest : BasePlatformTestCase() { 9 | 10 | fun testKsonFileWithoutErrors() { 11 | val psiFile = myFixture.configureByText(KsonFileType, "key: val") 12 | val ksonFile = assertInstanceOf(psiFile, KsonPsiFile::class.java) 13 | assertFalse(PsiErrorElementUtil.hasErrors(project, ksonFile.virtualFile)) 14 | } 15 | 16 | fun testKsonFileWithParseError() { 17 | val psiFileWithError = myFixture.configureByText(KsonFileType, "[\"unclosed list\", ") 18 | val ksonFileWithError = assertInstanceOf(psiFileWithError, KsonPsiFile::class.java) 19 | assertTrue( 20 | "should have errors --- since unclosed list error is created during parsing", 21 | PsiErrorElementUtil.hasErrors(project, ksonFileWithError.virtualFile) 22 | ) 23 | } 24 | 25 | fun testKsonFileWithPostProcessingIndentError() { 26 | val psiFile = myFixture.configureByText(KsonFileType, """ 27 | key: value 28 | bad_indent: value 29 | """.trimIndent()) 30 | val ksonFileWithError = assertInstanceOf(psiFile, KsonPsiFile::class.java) 31 | assertFalse( 32 | "should not have errors --- they are provided by an annotator", 33 | PsiErrorElementUtil.hasErrors(project, ksonFileWithError.virtualFile) 34 | ) 35 | 36 | // But the annotator should highlight the bad indentation as an error 37 | val highlights = myFixture.doHighlighting() 38 | assertTrue( 39 | "Annotator should detect bad indentation error", 40 | highlights.isNotEmpty() 41 | ) 42 | assertTrue( 43 | "Error message should mention indentation", 44 | highlights.any { it.description?.contains("indent", ignoreCase = true) == true } 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib-rust/kson/readme.md: -------------------------------------------------------------------------------- 1 | # Rust bindings for kson-lib API 2 | 3 | The Rust bindings for KSON are split into two crates, following the convention in the Rust 4 | ecosystem: 5 | 6 | - `kson-sys`: the low-level interface to the native library (you probably don't need to use it directly) 7 | - `kson-rs`: the idiomatic wrapper around kson (what you are probably looking for, see the example below) 8 | 9 | ## Example usage 10 | 11 | Add the library to your dependencies: 12 | 13 | ```bash 14 | cargo add kson-rs 15 | ``` 16 | 17 | Tell the build where to put the KSON shared library (see [below](#a-note-on-dynamic-linking) for 18 | details): 19 | 20 | ```bash 21 | export KSON_COPY_SHARED_LIBRARY_TO_DIR=target/debug 22 | ``` 23 | 24 | Write some code: 25 | 26 | ```rust 27 | use kson_rs::Kson; 28 | 29 | fn main() { 30 | let json = Kson::to_json("key: [1, 2, 3, 4]", false) 31 | .map_err(|_| "unreachable: kson input is guaranteed to be valid!") 32 | .unwrap(); 33 | println!("{}", json.output()); 34 | } 35 | ``` 36 | 37 | Running this with `cargo run` should print the following to stdout: 38 | 39 | ```json 40 | { 41 | "key": [ 42 | 1, 43 | 2, 44 | 3, 45 | 4 46 | ] 47 | } 48 | ``` 49 | 50 | ## Obtaining kson-lib binaries 51 | 52 | The `kson-sys` crate requires linking to the `kson-lib` binary. Our `build.rs` automatically 53 | downloads a suitable binary from the [kson-binaries 54 | repository](https://github.com/kson-org/kson-binaries), if it can be found. In case no pre-built 55 | binary is available for your platform, you need to manually specify how to obtain it through one of 56 | the following environment variables: 57 | 58 | * `KSON_ROOT_SOURCE_DIR`: if set to the root of a KSON source tree, we will attempt to build and use the necessary binaries from there. 59 | * `KSON_PREBUILT_BIN_DIR`: use pre-built KSON binaries from the specified directory. 60 | 61 | ## A note on dynamic linking 62 | 63 | The KSON bindings use dynamic linking, so you need to make sure the operating system can find the 64 | KSON library at runtime. Hence the `KSON_COPY_SHARED_LIBRARY_TO_DIR` trick, to put the library next 65 | to your binary. 66 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/KsonSettings.ts: -------------------------------------------------------------------------------- 1 | import {LSPAny} from "vscode-languageserver"; 2 | import {FormatOptions, FormattingStyle, IndentType} from "kson"; 3 | 4 | /** 5 | * Configuration settings for the Kson language server 6 | */ 7 | export interface KsonSettings { 8 | kson: { 9 | formatOptions: FormatOptions; 10 | }; 11 | } 12 | 13 | /** 14 | * Create new [KsonSettings] from LSP settings, applying defaults where needed. 15 | */ 16 | export function ksonSettingsWithDefaults(settings?: LSPAny): Required { 17 | // Create IndentType based on the provided settings 18 | let indentType: IndentType; 19 | if (settings?.kson?.format) { 20 | const format = settings.kson.format; 21 | if (format.insertSpaces === false) { 22 | indentType = IndentType.Tabs; 23 | } else { 24 | // Default to spaces with the specified or default tab size 25 | const tabSize = format.tabSize ?? 2; 26 | indentType = new IndentType.Spaces(tabSize); 27 | } 28 | } else { 29 | // Use the default from the Kotlin library 30 | indentType = new IndentType.Spaces(2); 31 | } 32 | 33 | // Create FormattingStyle based on the provided settings 34 | let formatStyle: FormattingStyle 35 | if (settings?.kson?.format?.formattingStyle) { 36 | // Map lowercase string to uppercase enum value exhaustively 37 | const style = settings.kson.format.formattingStyle.toLowerCase(); 38 | switch (style) { 39 | case 'plain': 40 | formatStyle = FormattingStyle.PLAIN; 41 | break; 42 | case 'delimited': 43 | formatStyle = FormattingStyle.DELIMITED; 44 | break; 45 | default: 46 | // Default to PLAIN for any unrecognized value 47 | formatStyle = FormattingStyle.PLAIN; 48 | break; 49 | } 50 | } else { 51 | formatStyle = FormattingStyle.PLAIN; 52 | } 53 | 54 | return { 55 | kson: { 56 | formatOptions: new FormatOptions(indentType, formatStyle) 57 | } 58 | }; 59 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/parser/MessageSink.kt: -------------------------------------------------------------------------------- 1 | package org.kson.parser 2 | 3 | import org.kson.stdlibx.collections.toImmutableList 4 | import org.kson.parser.messages.Message 5 | import org.kson.parser.messages.MessageSeverity 6 | 7 | data class LoggedMessage( 8 | val location: Location, 9 | val message: Message 10 | ) { 11 | companion object { 12 | /** 13 | * Print a user-friendly version of a [List] of [LoggedMessage]. 14 | * 15 | * Note: locations are output as base-1 indexed firstLine/firstColumn/lastLine/lastColumn numbers 16 | * following [the gnu standard](https://www.gnu.org/prep/standards/html_node/Errors.html) 17 | * for this sort of output 18 | */ 19 | fun print(loggedMessages: List): String { 20 | return loggedMessages.joinToString("\n") { loggedMessage -> 21 | val location = loggedMessage.location 22 | val severityLabel = when (loggedMessage.message.type.severity) { 23 | MessageSeverity.ERROR -> "Error" 24 | MessageSeverity.WARNING -> "Warning" 25 | } 26 | "$severityLabel:${location.start}" + 27 | " - ${location.end}, ${ 28 | loggedMessage.message 29 | }" 30 | } 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Collects messages (errors and warnings) during parsing and validation 37 | */ 38 | class MessageSink { 39 | private val messages = mutableSetOf() 40 | 41 | fun error(location: Location, message: Message) { 42 | messages.add(LoggedMessage(location, message)) 43 | } 44 | 45 | fun hasErrors(): Boolean { 46 | return messages.any { it.message.type.severity == MessageSeverity.ERROR } 47 | } 48 | 49 | fun hasMessages(): Boolean { 50 | return messages.isNotEmpty() 51 | } 52 | 53 | /** 54 | * Return the list of all [LoggedMessage]s sent to this [MessageSink], 55 | * in the order they were logged 56 | */ 57 | fun loggedMessages(): List { 58 | return messages.toImmutableList() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/features/CodeLensService.ts: -------------------------------------------------------------------------------- 1 | import { CodeLens } from 'vscode-languageserver'; 2 | import { KsonDocument } from '../document/KsonDocument.js'; 3 | import { CommandType } from '../commands/CommandType.js'; 4 | import { createTypedCommand } from '../commands/CommandParameters.js'; 5 | import { FormattingStyle } from 'kson'; 6 | 7 | /** 8 | * Service responsible for providing code lenses for Kson documents 9 | */ 10 | export class CodeLensService { 11 | 12 | /** 13 | * Get code lenses for a Kson document. 14 | */ 15 | getCodeLenses(document: KsonDocument): CodeLens[] { 16 | const formatCommand = createTypedCommand( 17 | CommandType.PLAIN_FORMAT, 18 | 'plain', 19 | { documentUri: document.uri, formattingStyle: FormattingStyle.PLAIN} 20 | ); 21 | 22 | const delimitedFormatCommand = createTypedCommand( 23 | CommandType.DELIMITED_FORMAT, 24 | 'delimited', 25 | { documentUri: document.uri, formattingStyle: FormattingStyle.DELIMITED } 26 | ); 27 | 28 | const compactFormatCommand = createTypedCommand( 29 | CommandType.COMPACT_FORMAT, 30 | 'compact', 31 | { documentUri: document.uri, formattingStyle: FormattingStyle.COMPACT } 32 | ); 33 | 34 | const classicFormatCommand = createTypedCommand( 35 | CommandType.CLASSIC_FORMAT, 36 | 'classic', 37 | { documentUri: document.uri, formattingStyle: FormattingStyle.CLASSIC } 38 | ); 39 | 40 | // Place both lenses at the start of the document 41 | const range = { 42 | start: { line: 0, character: 0 }, 43 | end: { line: 0, character: 0 } 44 | }; 45 | 46 | return [ 47 | { 48 | range, 49 | command: formatCommand 50 | }, 51 | { 52 | range, 53 | command: delimitedFormatCommand 54 | }, 55 | { 56 | range, 57 | command: classicFormatCommand 58 | }, 59 | { 60 | range, 61 | command: compactFormatCommand 62 | } 63 | ]; 64 | } 65 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/org/kson/parser/behavior/embedblock/EmbedBlockIndent.kt: -------------------------------------------------------------------------------- 1 | package org.kson.parser.behavior.embedblock 2 | 3 | 4 | /** 5 | * This class represents the behavior for handling embedded block content 6 | * by parsing and trimming its minimum indentation. 7 | * 8 | * @property rawEmbedContent The raw embedded block content as a string 9 | * to be analyzed or transformed. 10 | */ 11 | class EmbedBlockIndent(embedContent: String) { 12 | private val rawEmbedContent: String = embedContent 13 | 14 | /** 15 | * Computes the minimum indent of all lines in [rawEmbedContent], then returns 16 | * the text with that indent trimmed from each line. 17 | * 18 | * NOTE: blank lines are considered pure indent and used in this calculation, so for instance: 19 | * 20 | * " this string 21 | * has a minimum indent defined 22 | * by its last line 23 | * " 24 | * 25 | * becomes: 26 | * 27 | * " this string 28 | * has a minimum indent defined 29 | * by its blank last line 30 | * " 31 | */ 32 | fun trimMinimumIndent(): String { 33 | val minCommonIndent = computeMinimumIndent() 34 | 35 | return rawEmbedContent 36 | .split("\n") 37 | .joinToString("\n") { it.drop(minCommonIndent) } 38 | } 39 | 40 | /** 41 | * Computes the minimum indent in a [rawEmbedContent]. 42 | * 43 | * NOTE: blank lines are considered pure indent and used in this calculation, so for instance: 44 | * 45 | * " this string 46 | * has a minimum indent defined 47 | * by its last line 48 | * " 49 | * 50 | * returns: 2 51 | */ 52 | fun computeMinimumIndent(): Int { 53 | val linesWithNewlines = rawEmbedContent.split("\n").map { it + "\n" } 54 | 55 | val minCommonIndent = 56 | linesWithNewlines.minOfOrNull { it.indexOfFirst { char -> !isInlineWhitespace(char) } } ?: 0 57 | 58 | return minCommonIndent 59 | } 60 | 61 | /** 62 | * Returns true if the given [char] is a non-newline whitespace 63 | */ 64 | private fun isInlineWhitespace(char: Char?): Boolean { 65 | return char == ' ' || char == '\r' || char == '\t' 66 | } 67 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/parser/KsonParserDefinition.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.parser 2 | 3 | import com.intellij.lang.ASTNode 4 | import com.intellij.lang.ParserDefinition 5 | import com.intellij.lang.PsiParser 6 | import com.intellij.lexer.Lexer 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.psi.FileViewProvider 9 | import com.intellij.psi.PsiElement 10 | import com.intellij.psi.PsiFile 11 | import com.intellij.psi.tree.IFileElementType 12 | import com.intellij.psi.tree.TokenSet 13 | import org.kson.jetbrains.KsonLanguage 14 | import org.kson.jetbrains.psi.KsonPsiElement 15 | import org.kson.jetbrains.psi.KsonPsiFile 16 | import org.kson.parser.ParsedElementType 17 | import org.kson.parser.TokenType 18 | 19 | class KsonParserDefinition : ParserDefinition { 20 | override fun createLexer(project: Project?): Lexer { 21 | return KsonLexer() 22 | } 23 | 24 | override fun createParser(project: Project?): PsiParser { 25 | return KsonParser() 26 | } 27 | 28 | override fun getFileNodeType(): IFileElementType { 29 | return IFileElementType(KsonLanguage) 30 | } 31 | 32 | override fun getCommentTokens(): TokenSet { 33 | return commentTokenSet 34 | } 35 | 36 | override fun getWhitespaceTokens(): TokenSet { 37 | return whitespaceTokenSet 38 | } 39 | 40 | override fun getStringLiteralElements(): TokenSet { 41 | return stringTokenSet 42 | } 43 | 44 | override fun createElement(node: ASTNode): PsiElement { 45 | return when (node.elementType) { 46 | elem(ParsedElementType.EMBED_BLOCK) -> org.kson.jetbrains.psi.KsonEmbedBlock(node) 47 | elem(TokenType.EMBED_CONTENT) -> org.kson.jetbrains.psi.KsonEmbedContent(node) 48 | else -> KsonPsiElement(node) 49 | } 50 | } 51 | 52 | override fun createFile(viewProvider: FileViewProvider): PsiFile { 53 | return KsonPsiFile(viewProvider) 54 | } 55 | 56 | } 57 | 58 | private val commentTokenSet = TokenSet.create(elem(TokenType.COMMENT)) 59 | private val whitespaceTokenSet = TokenSet.create(elem(TokenType.WHITESPACE)) 60 | private val stringTokenSet = 61 | TokenSet.create(elem(ParsedElementType.OBJECT_KEY), elem(TokenType.STRING_CONTENT), elem(TokenType.UNQUOTED_STRING)) 62 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/org/kson/schema/validators/AnyOfValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package org.kson.schema.validators 2 | 3 | import org.kson.parser.messages.MessageType.* 4 | import org.kson.schema.JsonSchemaTest 5 | import kotlin.test.Test 6 | 7 | class AnyOfValidatorTest : JsonSchemaTest { 8 | @Test 9 | fun testAnyOfCommonValidationErrors() { 10 | assertKsonSchemaErrors( 11 | """ 12 | description: 99 13 | thing: false 14 | """.trimIndent(), 15 | """ 16 | anyOf: 17 | - properties: 18 | description: 19 | type: string 20 | . 21 | thing: 22 | type: number 23 | 24 | - properties: 25 | description: 26 | type: string 27 | . 28 | . 29 | required: 30 | - required_prop 31 | """.trimIndent(), 32 | listOf( 33 | // note that since `description` is wrong for ALL the sub-schemas, we get a precise error for it. 34 | SCHEMA_VALUE_TYPE_MISMATCH 35 | ) 36 | ) 37 | } 38 | 39 | @Test 40 | fun testAnyOfDiverseValidationErrors() { 41 | assertKsonSchemaErrors( 42 | """ 43 | description: "describer" 44 | think: false 45 | """.trimIndent(), 46 | """ 47 | anyOf: 48 | - properties: 49 | description: 50 | type: string 51 | . 52 | think: 53 | type: number 54 | 55 | - properties: 56 | description: 57 | type: string 58 | . 59 | . 60 | required: 61 | - required_prop 62 | """.trimIndent(), 63 | listOf( 64 | SCHEMA_ANY_OF_VALIDATION_FAILED, 65 | // sub-schema errors are rolled up into this error 66 | SCHEMA_SUB_SCHEMA_ERRORS) 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tooling/cli/src/main/kotlin/org/kson/tooling/cli/CommandLineInterface.kt: -------------------------------------------------------------------------------- 1 | package org.kson.tooling.cli 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parameters.options.versionOption 5 | import org.kson.tooling.cli.commands.JsonCommand 6 | import org.kson.tooling.cli.commands.YamlCommand 7 | import org.kson.tooling.cli.commands.ValidateCommand 8 | import org.kson.tooling.cli.commands.KsonFormatCommand 9 | import org.kson.tooling.cli.generated.KSON_VERSION 10 | 11 | 12 | class KsonCli : CliktCommand(name = "kson") { 13 | init { 14 | versionOption(KSON_VERSION, names = setOf("--version", "-V")) 15 | } 16 | override fun help(context: Context) = """ 17 | |KSON CLI - A tool for working with KSON (KSON Structured Object Notation) files. 18 | | 19 | |KSON is a human-friendly data serialization format that supports JSON and YAML conversion. 20 | |Use the subcommands to transpile, analyze, or validate KSON documents. 21 | | 22 | |Examples: 23 | |${"\u0085"}Convert KSON to JSON: 24 | |${"\u0085"} kson json -i input.kson -o output.json 25 | |${"\u0085"} 26 | |${"\u0085"}Convert KSON to YAML: 27 | |${"\u0085"} kson yaml -i input.kson -o output.yaml 28 | |${"\u0085"} 29 | |${"\u0085"}Format KSON with custom formatting options: 30 | |${"\u0085"} kson format -i input.kson --indent-spaces 4 -o formatted.kson 31 | |${"\u0085"} 32 | |${"\u0085"}Analyze KSON for errors: 33 | |${"\u0085"} kson analyze -i file.kson 34 | |${"\u0085"} 35 | |${"\u0085"}Validate against a schema: 36 | |${"\u0085"} kson json -i input.kson -s schema.kson -o output.json 37 | |${"\u0085"} 38 | |${"\u0085"}Read from stdin (use - or omit filename): 39 | |${"\u0085"} cat data.kson | kson json 40 | | 41 | |For more help on a specific command, use: kson --help 42 | """.trimMargin() 43 | 44 | init { 45 | context { 46 | allowInterspersedArgs = false 47 | } 48 | } 49 | 50 | override fun run() = Unit 51 | } 52 | 53 | fun main(args: Array) { 54 | KsonCli() 55 | .subcommands( 56 | KsonFormatCommand(), 57 | ValidateCommand(), 58 | JsonCommand(), 59 | YamlCommand(), 60 | ) 61 | .main(args) 62 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/main/kotlin/org/kson/jetbrains/pages/KsonColorSettingsPage.kt: -------------------------------------------------------------------------------- 1 | package org.kson.jetbrains.pages 2 | 3 | import com.intellij.openapi.editor.colors.TextAttributesKey 4 | import com.intellij.openapi.fileTypes.SyntaxHighlighter 5 | import com.intellij.openapi.options.colors.AttributesDescriptor 6 | import com.intellij.openapi.options.colors.ColorDescriptor 7 | import com.intellij.openapi.options.colors.ColorSettingsPage 8 | import org.kson.jetbrains.KsonBundle 9 | import org.kson.jetbrains.KsonIcons 10 | import org.kson.jetbrains.highlighter.KsonSyntaxHighlighter 11 | import javax.swing.Icon 12 | 13 | class KsonColorSettingsPage : ColorSettingsPage { 14 | override fun getIcon(): Icon { 15 | return KsonIcons.FILE 16 | } 17 | 18 | override fun getHighlighter(): SyntaxHighlighter { 19 | return KsonSyntaxHighlighter() 20 | } 21 | 22 | override fun getDemoText(): String { 23 | return """ 24 | key: value 25 | string: "a string" 26 | dashList: 27 | - "list element" 28 | - < 29 | - "element of delimited sub-list" 30 | - "another sub-list element" 31 | > 32 | - "another list element" 33 | list: [1, 2, 3, true, false] 34 | invalid: `` 35 | embed_block: %%kotlin 36 | println("Hello y'all") 37 | %% 38 | # this is a comment 39 | nested: { 40 | null_keyword: null 41 | } 42 | """.trimIndent() 43 | } 44 | 45 | override fun getAdditionalHighlightingTagToDescriptorMap(): Map? { 46 | return null 47 | } 48 | 49 | override fun getAttributeDescriptors(): Array { 50 | return DESCRIPTORS 51 | } 52 | 53 | override fun getColorDescriptors(): Array { 54 | return ColorDescriptor.EMPTY_ARRAY 55 | } 56 | 57 | override fun getDisplayName(): String { 58 | return KsonBundle.message("kson.name") 59 | } 60 | 61 | companion object { 62 | private val DESCRIPTORS = KsonSyntaxHighlighter.KsonColorTag.values().map { 63 | AttributesDescriptor( 64 | it.displayName, 65 | KsonSyntaxHighlighter.getTextAttributesKey(it) 66 | ) 67 | }.toTypedArray() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tooling/language-server-protocol/src/core/commands/RemoveSchemaCommand.ts: -------------------------------------------------------------------------------- 1 | import { SchemaConfigManager } from './SchemaConfigManager.js'; 2 | import { SCHEMA_CONFIG_FILENAME } from '../schema/SchemaConfig.js'; 3 | 4 | export interface RemoveSchemaParams { 5 | documentUri: string; 6 | workspaceRoot: string | null; 7 | } 8 | 9 | export interface RemoveSchemaResult { 10 | success: boolean; 11 | message: string; 12 | } 13 | 14 | /** 15 | * Removes a schema association from a document by updating the .kson-schema.kson configuration file. 16 | * 17 | * This command: 18 | * 1. Loads the .kson-schema.kson file 19 | * 2. Removes the schema mapping for the specified document 20 | * 3. Saves the updated configuration back to disk (or deletes the file if empty) 21 | */ 22 | export class RemoveSchemaCommand { 23 | /** 24 | * Execute the remove schema command. 25 | * 26 | * @param params Parameters containing document URI and workspace root 27 | * @returns Result indicating success or failure with a message 28 | */ 29 | static execute(params: RemoveSchemaParams): RemoveSchemaResult { 30 | const {documentUri, workspaceRoot} = params; 31 | 32 | if (!workspaceRoot) { 33 | return { 34 | success: false, 35 | message: 'No workspace root available. Cannot modify schema configuration.' 36 | }; 37 | } 38 | 39 | try { 40 | const configManager = SchemaConfigManager.tryLoad(workspaceRoot); 41 | 42 | if (!configManager) { 43 | return { 44 | success: false, 45 | message: `No schema configuration file found at ${SCHEMA_CONFIG_FILENAME}` 46 | }; 47 | } 48 | 49 | const removed = configManager.removeSchema(documentUri); 50 | 51 | if (!removed) { 52 | return { 53 | success: false, 54 | message: `No schema association found for document` 55 | }; 56 | } 57 | 58 | configManager.save(); 59 | 60 | return { 61 | success: true, 62 | message: `Removed schema association from document` 63 | }; 64 | } catch (error) { 65 | return { 66 | success: false, 67 | message: `Failed to remove schema association: ${error}` 68 | }; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /tooling/jetbrains/src/test/resources/testData/parser/EmptyCommaError.txt: -------------------------------------------------------------------------------- 1 | FILE(0,28) 2 | KsonPsiElement([Kson-parsed] BRACKET_LIST)(0,28) 3 | PsiElement(IElementTokenType(tokenType=SQUARE_BRACKET_L))('[')(0,1) 4 | PsiErrorElement:Redundant comma found. A comma must delimit a value, one comma per value(1,5) 5 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(1,2) 6 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(2,3) 7 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(3,4) 8 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(4,5) 9 | KsonPsiElement([Kson-parsed] LIST_ELEMENT)(6,10) 10 | KsonPsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))(6,7) 11 | PsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))('x')(6,7) 12 | PsiErrorElement:Redundant comma found. A comma must delimit a value, one comma per value(8,10) 13 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(8,9) 14 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(9,10) 15 | KsonPsiElement([Kson-parsed] LIST_ELEMENT)(11,20) 16 | KsonPsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))(11,12) 17 | PsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))('y')(11,12) 18 | PsiErrorElement:Redundant comma found. A comma must delimit a value, one comma per value(13,20) 19 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(13,14) 20 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(14,15) 21 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(15,16) 22 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(16,17) 23 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(17,18) 24 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(18,19) 25 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(19,20) 26 | KsonPsiElement([Kson-parsed] LIST_ELEMENT)(21,27) 27 | KsonPsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))(21,22) 28 | PsiElement(IElementTokenType(tokenType=UNQUOTED_STRING))('z')(21,22) 29 | PsiErrorElement:Redundant comma found. A comma must delimit a value, one comma per value(23,27) 30 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(23,24) 31 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(24,25) 32 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(25,26) 33 | PsiElement(IElementTokenType(tokenType=COMMA))(',')(26,27) 34 | PsiElement(IElementTokenType(tokenType=SQUARE_BRACKET_R))(']')(27,28) 35 | --------------------------------------------------------------------------------