├── src ├── accessor │ ├── number.ts │ ├── index.ts │ └── operator.ts ├── static-h.ts ├── tests │ ├── resources │ │ ├── setup │ │ │ ├── index.ts │ │ │ ├── order.tsx │ │ │ └── package.tsx │ │ ├── cases │ │ │ ├── order.kdl │ │ │ └── package.kdl │ │ └── generate.tsx │ ├── tokenizer.ts │ ├── index.tsx │ ├── readme.tsx │ ├── prepare.tsx │ └── package.tsx ├── index.ts ├── types │ └── jsx.d.ts ├── tokenizer │ ├── tokens │ │ ├── index.ts │ │ ├── query │ │ │ └── index.ts │ │ └── query-accessor │ │ │ └── index.ts │ └── index.ts ├── h.ts ├── query.ts ├── is.ts ├── string.ts └── prepare.ts ├── scripts ├── imported │ ├── config.js │ ├── package.json │ └── index.js ├── example.js ├── log-version.js ├── nop │ ├── package.json │ └── index.js ├── correct-import-extensions.js └── post-build.js ├── .npmignore ├── .gitignore ├── bun.lockb ├── .nycrc ├── CONTRIBUTING.md ├── tsconfig.json ├── .github └── workflows │ ├── test-actions.yml │ ├── release-actions.yml │ └── codeql-analysis.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── LICENSE.md ├── import-map-deno.json ├── package.json ├── README.md └── CODE-OF-CONDUCT.md /src/accessor/number.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/imported/config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/accessor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./operator"; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | coverage -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | import "../esnext/example/index.js"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage -------------------------------------------------------------------------------- /src/static-h.ts: -------------------------------------------------------------------------------- 1 | export * from "@virtualstate/focus/static-h"; 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virtualstate/kdl/main/bun.lockb -------------------------------------------------------------------------------- /src/tests/resources/setup/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./package"; 2 | export * from "./order"; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./string"; 2 | export * from "./query"; 3 | export * from "./prepare"; 4 | export * from "./static-h"; 5 | -------------------------------------------------------------------------------- /src/types/jsx.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface IntrinsicElements 3 | extends Record> {} 4 | } 5 | -------------------------------------------------------------------------------- /scripts/log-version.js: -------------------------------------------------------------------------------- 1 | import("fs") 2 | .then(({ promises }) => promises.readFile("package.json")) 3 | .then(JSON.parse) 4 | .then(({ version }) => console.log(version)); 5 | -------------------------------------------------------------------------------- /scripts/nop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/imported/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "main": "./index.js", 4 | "type": "module", 5 | "exports": { 6 | ".": "./index.js", 7 | "./": "./index.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tokenizer/tokens/index.ts: -------------------------------------------------------------------------------- 1 | export { QueryParseTokens } from "./query"; 2 | export * as Query from "./query"; 3 | export { QueryAccessorParseTokens } from "./query-accessor"; 4 | export * as QueryAccessor from "./query-accessor"; 5 | -------------------------------------------------------------------------------- /src/h.ts: -------------------------------------------------------------------------------- 1 | import { f } from "@virtualstate/fringe"; 2 | 3 | export function h( 4 | tag: string | symbol, 5 | options?: Record, 6 | ...children: unknown[] 7 | ): unknown { 8 | return f(tag, options, children); 9 | } 10 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ], 4 | "reporter": [ 5 | "clover", 6 | "json-summary", 7 | "html", 8 | "text-summary" 9 | ], 10 | "branches": 80, 11 | "lines": 80, 12 | "functions": 80, 13 | "statements": 80 14 | } 15 | -------------------------------------------------------------------------------- /src/tests/tokenizer.ts: -------------------------------------------------------------------------------- 1 | import { query } from "../tokenizer"; 2 | 3 | for (const token of query( 4 | `name > top() [prop("a") = 1] another(tagged)[values()] with[type="checkbox"][checked=true] || with[type="checkbox"][checked=false] || input[type="number"][val()=1][val()<5]` 5 | )) { 6 | console.log(token); 7 | } 8 | -------------------------------------------------------------------------------- /scripts/nop/index.js: -------------------------------------------------------------------------------- 1 | export const listenAndServe = undefined; 2 | export const createServer = undefined; 3 | 4 | /** 5 | * @type {any} 6 | */ 7 | const on = () => {}; 8 | /** 9 | * @type {any} 10 | */ 11 | const env = {}; 12 | 13 | /*** 14 | * @type {any} 15 | */ 16 | export default { 17 | env, 18 | on, 19 | exit(arg) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/tests/resources/cases/order.kdl: -------------------------------------------------------------------------------- 1 | queries name="order" { 2 | input { 3 | order product={product name="string" sku="string"} quantity="number" 4 | } 5 | query "order" index=0 { 6 | output { 7 | order product={product name="string" sku="string"} quantity="number" 8 | } 9 | "@virtualstate/kdl/output/match" true 10 | } 11 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by way of an issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | We are open to all ideas big or small, and are greatly appreciative of any and all contributions. 7 | 8 | Please note we have a code of conduct, please follow it in all your interactions with the project. 9 | -------------------------------------------------------------------------------- /src/tests/index.tsx: -------------------------------------------------------------------------------- 1 | import { toKDLString } from "../index"; 2 | import { h } from "../static-h"; 3 | import { createToken } from "@virtualstate/fringe"; 4 | import { rawKDLQuery } from "../query"; 5 | 6 | try { 7 | await import("./resources/generate"); 8 | } catch (error) { 9 | // console.error(error); 10 | } 11 | 12 | await import("./package"); 13 | await import("./tokenizer"); 14 | await import("./prepare"); 15 | await import("./readme"); 16 | -------------------------------------------------------------------------------- /scripts/imported/index.js: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | const initialImportPath = 4 | getConfig()["@virtualstate/app-history/test/imported/path"] ?? 5 | "@virtualstate/app-history"; 6 | 7 | if (typeof initialImportPath !== "string") 8 | throw new Error("Expected string import path"); 9 | 10 | export const { AppHistory } = await import(initialImportPath); 11 | 12 | export function getConfig() { 13 | return { 14 | ...getNodeConfig(), 15 | }; 16 | } 17 | 18 | function getNodeConfig() { 19 | if (typeof process === "undefined") return {}; 20 | return JSON.parse(process.env.TEST_CONFIG ?? "{}"); 21 | } 22 | /* c8 ignore end */ 23 | -------------------------------------------------------------------------------- /src/tests/resources/setup/order.tsx: -------------------------------------------------------------------------------- 1 | import { h } from "@virtualstate/focus"; 2 | import { Outputs } from "./package"; 3 | import { ResolveObjectAttributes } from "../../../string"; 4 | 5 | const Product = ; 6 | const Order = } quantity="number" />; 7 | 8 | export const orderDocument = ; 9 | 10 | export const orderQueries = ["order"] as const; 11 | 12 | export const orderOutputs: Outputs = [orderDocument]; 13 | 14 | export const orderOptions = { 15 | /** 16 | * @experimental 17 | */ 18 | [ResolveObjectAttributes]: true, 19 | } as const; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["es2018", "esnext", "dom"], 5 | "types": ["jest", "node"], 6 | "esModuleInterop": true, 7 | "target": "esnext", 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "esnext", 14 | "baseUrl": "./src", 15 | "jsx": "react", 16 | "jsxFactory": "h", 17 | "jsxFragmentFactory": "createFragment", 18 | "allowJs": true, 19 | "paths": {} 20 | }, 21 | "include": ["src/**/*"], 22 | "typeRoots": ["./node_modules/@types", "src/types"], 23 | "exclude": ["node_modules/@opennetwork/vdom"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/test-actions.yml: -------------------------------------------------------------------------------- 1 | name: test-actions 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | env: 8 | NO_COVERAGE_BADGE_UPDATE: 1 9 | FLAGS: FETCH_SERVICE_DISABLE,POST_CONFIGURE_TEST,PLAYWRIGHT,CONTINUE_ON_ERROR 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: "16.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: "v1.x" 21 | - run: | 22 | yarn install 23 | npx playwright install-deps 24 | - run: yarn build 25 | # yarn coverage === c8 + yarn test 26 | - run: yarn coverage 27 | - run: yarn test:deno 28 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 16, 14, 12 4 | ARG VARIANT="16-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import { prepare } from "./prepare"; 2 | 3 | export function runKDLQuery(query: string, input?: unknown) { 4 | const Query = rawKDLQuery(query); 5 | return { 6 | name: Symbol.for(":kdl/fragment"), 7 | children: { 8 | [Symbol.asyncIterator]() { 9 | return Query({}, input)[Symbol.asyncIterator](); 10 | }, 11 | }, 12 | }; 13 | } 14 | 15 | export function rawKDLQuery( 16 | input: string | TemplateStringsArray, 17 | ...args: unknown[] 18 | ) { 19 | let query: string; 20 | if (typeof input === "string") { 21 | query = input; 22 | } else { 23 | query = input.reduce((string, value, index) => { 24 | return `${string}${value}${args[index] ?? ""}`; 25 | }, ""); 26 | } 27 | return function ( 28 | options: Record, 29 | input?: unknown 30 | ): AsyncIterable { 31 | return prepare(input, query); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 12, 14, 16 8 | "args": { 9 | "VARIANT": "16" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": ["dbaeumer.vscode-eslint"], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "yarn install", 24 | 25 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 26 | "remoteUser": "node" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Axiom Applied Technologies and Development Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/is.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | 3 | import { isLike } from "@virtualstate/focus"; 4 | 5 | export function isAsyncIterable(value: unknown): value is AsyncIterable { 6 | return !!( 7 | isLike>(value) && 8 | typeof value[Symbol.asyncIterator] === "function" 9 | ); 10 | } 11 | 12 | export function isIterable(value: unknown): value is Iterable { 13 | return !!( 14 | isLike>(value) && 15 | typeof value[Symbol.iterator] === "function" 16 | ); 17 | } 18 | 19 | export function isIteratorYieldResult( 20 | value: unknown 21 | ): value is IteratorYieldResult { 22 | return !!( 23 | isLike>>(value) && 24 | typeof value.done === "boolean" && 25 | !value.done 26 | ); 27 | } 28 | 29 | export function isIteratorResult( 30 | value: unknown 31 | ): value is IteratorYieldResult { 32 | return !!( 33 | isLike>>(value) && typeof value.done === "boolean" 34 | ); 35 | } 36 | 37 | export function isRejected( 38 | value: PromiseSettledResult 39 | ): value is PromiseRejectedResult; 40 | export function isRejected( 41 | value: PromiseSettledResult 42 | ): value is R; 43 | export function isRejected( 44 | value: PromiseSettledResult 45 | ): value is R { 46 | return value?.status === "rejected"; 47 | } 48 | 49 | export function isFulfilled( 50 | value: PromiseSettledResult 51 | ): value is PromiseFulfilledResult { 52 | return value?.status === "fulfilled"; 53 | } 54 | 55 | export function isPromise(value: unknown): value is Promise { 56 | return isLike>(value) && typeof value.then === "function"; 57 | } 58 | 59 | export function isArray(value: unknown): value is T[] { 60 | return Array.isArray(value); 61 | } 62 | 63 | export function isDefined(value?: T): value is T { 64 | return typeof value !== "undefined"; 65 | } 66 | -------------------------------------------------------------------------------- /src/tokenizer/tokens/query/index.ts: -------------------------------------------------------------------------------- 1 | import Chevrotain from "chevrotain"; 2 | import type { TokenType } from "chevrotain"; 3 | 4 | const { createToken } = Chevrotain; 5 | 6 | export const QueryParseTokens: TokenType[] = []; 7 | 8 | export const WhiteSpace = createToken({ 9 | name: "WhiteSpace", 10 | pattern: /[\s\n]+/, 11 | }); 12 | QueryParseTokens.push(WhiteSpace); 13 | export const Raw = createToken({ 14 | name: "Raw", 15 | pattern: /r#".*"#/, 16 | line_breaks: true, 17 | }); 18 | QueryParseTokens.push(Raw); 19 | export const String = createToken({ 20 | name: "String", 21 | pattern: /"[^"]*"/, 22 | line_breaks: true, 23 | }); 24 | QueryParseTokens.push(String); 25 | export const Number = createToken({ 26 | name: "Number", 27 | pattern: /[+-]?\d+(?:\.\d+)?/, 28 | }); 29 | QueryParseTokens.push(Number); 30 | export const Boolean = createToken({ 31 | name: "Boolean", 32 | pattern: /true|false/, 33 | }); 34 | QueryParseTokens.push(Boolean); 35 | 36 | export const Accessor = createToken({ 37 | name: "Accessor", 38 | pattern: /\[[^\]]*]/, 39 | }); 40 | QueryParseTokens.push(Accessor); 41 | export const Top = createToken({ 42 | name: "Top", 43 | pattern: /top\(\)/, 44 | }); 45 | QueryParseTokens.push(Top); 46 | 47 | export const Selector = createToken({ 48 | name: "Selector", 49 | pattern: /[a-z]+[^(\[\s]*/i, 50 | }); 51 | QueryParseTokens.push(Selector); 52 | 53 | export const Tag = createToken({ 54 | name: "Tag", 55 | pattern: /\([^)]*\)?/i, 56 | }); 57 | QueryParseTokens.push(Tag); 58 | 59 | export const Or = createToken({ 60 | name: "Or", 61 | pattern: /\|\|/, 62 | }); 63 | QueryParseTokens.push(Or); 64 | export const ImmediatelyFollows = createToken({ 65 | name: "ImmediatelyFollows", 66 | pattern: /\+/, 67 | }); 68 | QueryParseTokens.push(ImmediatelyFollows); 69 | export const Follows = createToken({ 70 | name: "Follows", 71 | pattern: /~/, 72 | }); 73 | QueryParseTokens.push(Follows); 74 | 75 | export const Map = createToken({ 76 | name: "Map", 77 | pattern: /=> .+/, 78 | }); 79 | QueryParseTokens.push(Map); 80 | 81 | export const DirectChild = createToken({ 82 | name: "DirectChild", 83 | pattern: />/, 84 | }); 85 | QueryParseTokens.push(DirectChild); 86 | -------------------------------------------------------------------------------- /src/tests/readme.tsx: -------------------------------------------------------------------------------- 1 | import { prepare } from "../prepare"; 2 | import {h, name, ok} from "@virtualstate/focus"; 3 | 4 | const node = ( 5 |
6 |

@virtualstate/focus

7 |
Version: 1.0.0
8 |
9 | ); 10 | 11 | { 12 | const result = prepare( 13 | node, 14 | `main blockquote > span` 15 | ); 16 | const [span] = await result 17 | console.log(span); // Is the node for 1.0.0 18 | ok(name(span) === "span"); 19 | 20 | } 21 | 22 | { 23 | const result = prepare( 24 | node, 25 | `main blockquote > span` 26 | ); 27 | let count = 0; 28 | for await (const [span] of result) { 29 | if (!span) continue; 30 | // We have at least one span! 31 | console.log(span) // Is the node for 1.0.0 32 | ok(name(span) === "span"); 33 | count += 1; 34 | } 35 | ok(count); 36 | 37 | } 38 | { 39 | const result = prepare( 40 | node, 41 | `main blockquote > span` 42 | ); 43 | const [firstSpan] = result; 44 | const span = await firstSpan; 45 | console.log(span) // Is the node for 1.0.0 46 | ok(name(span) === "span"); 47 | 48 | } 49 | { 50 | const result = prepare( 51 | node, 52 | `main blockquote > span` 53 | ); 54 | const [firstSpan] = result; 55 | let count = 0; 56 | for await (const span of firstSpan) { 57 | console.log(span) // Is the node for 1.0.0 58 | ok(name(span) === "span"); 59 | count += 1; 60 | } 61 | ok(count); 62 | 63 | } 64 | 65 | { 66 | 67 | const result = prepare( 68 | node, 69 | `main blockquote > span` 70 | ); 71 | ok(typeof result.at === "function"); 72 | ok(typeof result.filter === "function"); 73 | ok(typeof result.map === "function"); 74 | ok(typeof result.group === "function"); 75 | ok(typeof result.flatMap === "function"); 76 | } 77 | 78 | { 79 | { 80 | 81 | const [value] = await prepare( 82 | node, 83 | `main blockquote > span => val()` 84 | ); 85 | 86 | console.log(value); 87 | ok(value === "1.0.0"); 88 | } 89 | } -------------------------------------------------------------------------------- /.github/workflows/release-actions.yml: -------------------------------------------------------------------------------- 1 | name: release-actions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | release: 7 | types: 8 | - created 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | env: 13 | NO_COVERAGE_BADGE_UPDATE: 1 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16.x" 21 | registry-url: "https://registry.npmjs.org" 22 | - uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: "v1.x" 25 | - run: | 26 | yarn install 27 | - run: yarn build 28 | # yarn coverage === c8 + yarn test 29 | - run: yarn coverage 30 | - run: yarn test:deno 31 | - name: Package Registry Publish - npm 32 | run: | 33 | git config user.name "${{ github.actor }}" 34 | git config user.email "${{ github.actor}}@users.noreply.github.com" 35 | npm set "registry=https://registry.npmjs.org/" 36 | npm set "@virtualstate:registry=https://registry.npmjs.org/" 37 | npm set "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" 38 | npm publish --access=public 39 | continue-on-error: true 40 | env: 41 | YARN_TOKEN: ${{ secrets.YARN_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.YARN_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }} 44 | - uses: actions/setup-node@v2 45 | with: 46 | node-version: "16.x" 47 | registry-url: "https://npm.pkg.github.com" 48 | - name: Package Registry Publish - GitHub 49 | run: | 50 | git config user.name "${{ github.actor }}" 51 | git config user.email "${{ github.actor}}@users.noreply.github.com" 52 | npm set "registry=https://npm.pkg.github.com/" 53 | npm set "@virtualstate:registry=https://npm.pkg.github.com/virtualstate" 54 | npm set "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" 55 | npm publish --access=public 56 | env: 57 | YARN_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | continue-on-error: true 61 | -------------------------------------------------------------------------------- /src/tests/resources/setup/package.tsx: -------------------------------------------------------------------------------- 1 | import { h, named, createFragment } from "../../../static-h"; 2 | 3 | const Package = named("package"); 4 | 5 | const name = foo; 6 | const version = 1.0.0; 7 | const windowsDependencies = ( 8 | 9 | 1.0.0 10 | 11 | ); 12 | const defaultDependencies = ( 13 | 14 | 2.0.0 15 | 16 | ); 17 | const dependencies = ( 18 | <> 19 | {windowsDependencies} 20 | {defaultDependencies} 21 | 22 | ); 23 | const lastBuiltAt = {12345}; 24 | 25 | export const packageDocument = ( 26 | 27 | {name} 28 | {version} 29 | {dependencies} 30 | {lastBuiltAt} 31 | 32 | ); 33 | 34 | export const packageQueries = [ 35 | `top() > package dependencies[prop(platform) = "windows"]`, 36 | `top() > package dependencies[prop(platform) != "windows"]`, 37 | `top() > package dependencies[prop(platform) != r#"windows"#] || top() > package dependencies[prop(platform) = "windows"]`, 38 | `dependencies`, 39 | `dependencies[prop(platform) ^= "win"]`, 40 | `dependencies[prop(platform) $= "s"]`, 41 | `dependencies[prop(platform) *= "in"]`, 42 | `dependencies[prop(platform)]`, 43 | `dependencies[platform]`, 44 | `dependencies[platform = "windows"]`, 45 | `lastBuiltAt[val() > 0]`, 46 | `lastBuiltAt[val() >= 0]`, 47 | `lastBuiltAt[val() < 9999999]`, 48 | `lastBuiltAt[val() <= 9999999]`, 49 | `dependencies + dependencies`, 50 | `version ~ dependencies`, 51 | `dependencies[prop(platform) ^= "win"] top() package > version`, 52 | `version`, 53 | `name`, 54 | "lastBuiltAt[val()]", 55 | ] as const; 56 | 57 | export type Outputs> = { 58 | [K in keyof Queries]: unknown; 59 | } & { length: Queries["length"] }; 60 | 61 | export const packageOutputs: Outputs = [ 62 | windowsDependencies, 63 | defaultDependencies, 64 | <> 65 | {defaultDependencies} 66 | {windowsDependencies} 67 | , 68 | dependencies, 69 | windowsDependencies, 70 | windowsDependencies, 71 | windowsDependencies, 72 | windowsDependencies, 73 | windowsDependencies, 74 | windowsDependencies, 75 | lastBuiltAt, 76 | lastBuiltAt, 77 | lastBuiltAt, 78 | lastBuiltAt, 79 | defaultDependencies, 80 | dependencies, 81 | version, 82 | version, 83 | name, 84 | lastBuiltAt, 85 | ] as const; 86 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '15 10 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /import-map-deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@opennetwork/http-representation": "https://cdn.skypack.dev/@opennetwork/http-representation", 4 | "@virtualstate/astro-renderer": "https://cdn.skypack.dev/@virtualstate/astro-renderer", 5 | "@virtualstate/dom": "https://cdn.skypack.dev/@virtualstate/dom", 6 | "@virtualstate/examples": "https://cdn.skypack.dev/@virtualstate/examples", 7 | "@virtualstate/fringe": "https://cdn.skypack.dev/@virtualstate/fringe", 8 | "@virtualstate/focus": "https://cdn.skypack.dev/@virtualstate/focus", 9 | "@virtualstate/focus/static-h": "https://cdn.skypack.dev/@virtualstate/focus/static-h", 10 | "@virtualstate/hooks": "https://cdn.skypack.dev/@virtualstate/hooks", 11 | "@virtualstate/hooks-extended": "https://cdn.skypack.dev/@virtualstate/hooks-extended", 12 | "@virtualstate/union": "https://cdn.skypack.dev/@virtualstate/union", 13 | "@virtualstate/x": "https://cdn.skypack.dev/@virtualstate/x", 14 | "@virtualstate/promise": "https://cdn.skypack.dev/@virtualstate/promise", 15 | "chevrotain": "https://cdn.skypack.dev/chevrotain", 16 | "@virtualstate/promise/the-thing": "https://cdn.skypack.dev/@virtualstate/promise/the-thing", 17 | "@virtualstate/app-history": "./src/app-history.ts", 18 | "@virtualstate/app-history/polyfill": "./src/polyfill.ts", 19 | "@virtualstate/app-history/event-target/sync": "./src/event-target/sync-event-target.ts", 20 | "@virtualstate/app-history/event-target/async": "./src/event-target/async-event-target.ts", 21 | "@virtualstate/app-history/event-target": "./src/event-target/sync-event-target.ts", 22 | "@virtualstate/app-history-imported": "./src/app-history.ts", 23 | "@virtualstate/app-history-imported/polyfill": "./src/polyfill.ts", 24 | "@virtualstate/app-history-imported/event-target/sync": "./src/event-target/sync-event-target.ts", 25 | "@virtualstate/app-history-imported/event-target/async": "./src/event-target/async-event-target.ts", 26 | "@virtualstate/app-history-imported/event-target": "./src/event-target/sync-event-target.ts", 27 | "dom-lite": "https://cdn.skypack.dev/dom-lite", 28 | "iterable": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 29 | "https://cdn.skypack.dev/-/iterable@v5.7.0-CNtyuMJo9f2zFu6CuB1D/dist=es2019,mode=imports/optimized/iterable.js": "https://cdn.skypack.dev/iterable@6.0.1-beta.5", 30 | "uuid": "./src/util/deno-uuid.ts", 31 | "whatwg-url": "https://cdn.skypack.dev/whatwg-url", 32 | "abort-controller": "https://cdn.skypack.dev/abort-controller", 33 | "deno:std/uuid/mod": "https://deno.land/std@0.113.0/uuid/mod.ts", 34 | "deno:std/uuid/v4": "https://deno.land/std@0.113.0/uuid/v4.ts", 35 | "deno:deno_dom/deno-dom-wasm.ts": "https://deno.land/x/deno_dom/deno-dom-wasm.ts", 36 | "urlpattern-polyfill": "https://cdn.skypack.dev/urlpattern-polyfill", 37 | "cheerio": "./scripts/nop/index.js" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/correct-import-extensions.js: -------------------------------------------------------------------------------- 1 | import FileHound from "filehound"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | import { promisify } from "util"; 5 | // 6 | // const packages = await FileHound.create() 7 | // .paths(`packages`) 8 | // .directory() 9 | // .depth(1) 10 | // .find(); 11 | 12 | const buildPaths = ["esnext"]; 13 | 14 | for (const buildPath of buildPaths) { 15 | const filePaths = await FileHound.create() 16 | .paths(buildPath) 17 | .discard("node_modules") 18 | .ext("js") 19 | .find(); 20 | 21 | await Promise.all( 22 | filePaths.map(async (filePath) => { 23 | const initialContents = await fs.readFile(filePath, "utf-8"); 24 | 25 | const statements = initialContents.match( 26 | /(?:(?:import|export)(?: .+ from)? ".+";|(?:import\(".+"\)))/g 27 | ); 28 | 29 | if (!statements) { 30 | return; 31 | } 32 | 33 | const importMap = process.env.IMPORT_MAP 34 | ? JSON.parse(await fs.readFile(process.env.IMPORT_MAP, "utf-8")) 35 | : undefined; 36 | const contents = await statements.reduce( 37 | async (contentsPromise, statement) => { 38 | const contents = await contentsPromise; 39 | const url = statement.match(/"(.+)"/)[1]; 40 | if (importMap?.imports?.[url]) { 41 | const replacement = importMap.imports[url]; 42 | if (!replacement.includes("./src")) { 43 | return contents.replace( 44 | statement, 45 | statement.replace(url, replacement) 46 | ); 47 | } 48 | const shift = filePath 49 | .split("/") 50 | // Skip top folder + file 51 | .slice(2) 52 | // Replace with shift up directory 53 | .map(() => "..") 54 | .join("/"); 55 | return contents.replace( 56 | statement, 57 | statement.replace( 58 | url, 59 | replacement.replace("./src", shift).replace(/\.tsx?$/, ".js") 60 | ) 61 | ); 62 | } else { 63 | return contents.replace(statement, await getReplacement(url)); 64 | } 65 | 66 | async function getReplacement(url) { 67 | const [stat, indexStat] = await Promise.all([ 68 | fs 69 | .stat(path.resolve(path.dirname(filePath), url + ".js")) 70 | .catch(() => {}), 71 | fs 72 | .stat(path.resolve(path.dirname(filePath), url + "/index.js")) 73 | .catch(() => {}), 74 | ]); 75 | 76 | if (stat && stat.isFile()) { 77 | return statement.replace(url, url + ".js"); 78 | } else if (indexStat && indexStat.isFile()) { 79 | return statement.replace(url, url + "/index.js"); 80 | } 81 | return statement; 82 | } 83 | }, 84 | Promise.resolve(initialContents) 85 | ); 86 | 87 | await fs.writeFile(filePath, contents, "utf-8"); 88 | }) 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtualstate/kdl", 3 | "version": "1.0.4", 4 | "main": "./esnext/index.js", 5 | "module": "./esnext/index.js", 6 | "types": "./esnext/index.d.ts", 7 | "type": "module", 8 | "sideEffects": false, 9 | "keywords": [], 10 | "exports": { 11 | ".": "./esnext/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/virtualstate/kdl.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/virtualstate/kdl/issues" 19 | }, 20 | "homepage": "https://github.com/virtualstate/kdl#readme", 21 | "author": "Fabian Cook ", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@virtualstate/focus": "^1.0.1", 25 | "@virtualstate/promise": "^1.1.5", 26 | "abort-controller": "^3.0.0", 27 | "chevrotain": "^10.1.2", 28 | "uuid": "^8.3.2", 29 | "whatwg-url": "^9.1.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/cli": "^7.15.4", 33 | "@babel/core": "^7.15.4", 34 | "@babel/preset-env": "^7.15.4", 35 | "@opennetwork/http-representation": "^3.0.0", 36 | "@rollup/plugin-node-resolve": "^13.1.1", 37 | "@rollup/plugin-typescript": "^8.3.0", 38 | "@types/chance": "^1.1.3", 39 | "@types/jest": "^27.0.1", 40 | "@types/mkdirp": "^1.0.2", 41 | "@types/node": "^17.0.1", 42 | "@types/rimraf": "^3.0.2", 43 | "@types/uuid": "^8.3.3", 44 | "@types/whatwg-url": "^8.2.1", 45 | "@virtualstate/dom": "^2.46.0", 46 | "@virtualstate/examples": "^2.46.0", 47 | "@virtualstate/fringe": "^2.46.1", 48 | "@virtualstate/hooks": "^2.46.0", 49 | "@virtualstate/hooks-extended": "^2.46.0", 50 | "@virtualstate/union": "^2.46.0", 51 | "@virtualstate/x": "^2.46.0", 52 | "c8": "^7.12.0", 53 | "chance": "^1.1.8", 54 | "cheerio": "^1.0.0-rc.10", 55 | "core-js": "^3.17.2", 56 | "dom-lite": "^20.2.0", 57 | "filehound": "^1.17.4", 58 | "jest": "^27.1.0", 59 | "jest-playwright-preset": "^1.7.0", 60 | "mkdirp": "^1.0.4", 61 | "playwright": "^1.17.1", 62 | "rimraf": "^3.0.2", 63 | "rollup": "^2.61.1", 64 | "rollup-plugin-babel": "^4.4.0", 65 | "rollup-plugin-ignore": "^1.0.10", 66 | "ts-jest": "^27.0.5", 67 | "ts-node": "^10.2.1", 68 | "typescript": "^4.7.4", 69 | "urlpattern-polyfill": "^1.0.0-rc2", 70 | "v8-to-istanbul": "^8.1.0" 71 | }, 72 | "scripts": { 73 | "build": "rm -rf esnext && tsc", 74 | "postbuild": "mkdir -p coverage && node scripts/post-build.js", 75 | "generate": "yarn build && yarn test:generate", 76 | "test:generate": "node esnext/tests/resources/generate.js", 77 | "prepublishOnly": "npm run build", 78 | "test": "yarn build && yarn test:generate && node --enable-source-maps esnext/tests/index.js", 79 | "test:deno": "yarn build && deno run --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 80 | "test:deno:r": "yarn build && deno run -r --allow-read --allow-net --import-map=import-map-deno.json esnext/tests/index.js", 81 | "test:inspect": "yarn build && node --enable-source-maps --inspect-brk esnext/tests/index.js", 82 | "coverage": "yarn build && c8 node esnext/tests/index.js && yarn postbuild" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/accessor/operator.ts: -------------------------------------------------------------------------------- 1 | export const operators = { 2 | notEquals: "!=", 3 | startsWithString: "^=", 4 | endsWithString: "$=", 5 | containsString: "*=", 6 | greaterThanOrEqualsNumber: ">=", 7 | lessThanOrEqualsNumber: "<=", 8 | greaterThanNumber: ">", 9 | lessThanNumber: "<", 10 | equals: "=", 11 | } as const; 12 | export type Operation = typeof operators[keyof typeof operators]; 13 | export const operatorSymbols: Operation[] = Object.values(operators).sort( 14 | (a, b) => (a.length < b.length ? 1 : -1) 15 | ); 16 | 17 | const unknownOperatorSymbols: unknown[] = operatorSymbols; 18 | 19 | export function isOperation(value: unknown): value is Operation { 20 | return unknownOperatorSymbols.includes(value); 21 | } 22 | 23 | export function operator(left: unknown, right: unknown, op: Operation) { 24 | // console.log({ left, right, op }, Object.keys(operators).find((key: keyof typeof operators) => operators[key] === op)); 25 | 26 | if (op === operators.equals) { 27 | return left === right; 28 | } 29 | 30 | if (op === operators.notEquals) { 31 | return left !== right; 32 | } 33 | 34 | const number = numberOperator(); 35 | 36 | if (typeof number === "boolean") { 37 | return number; 38 | } 39 | 40 | const string = stringOperator(); 41 | 42 | if (typeof string === "boolean") { 43 | return string; 44 | } 45 | 46 | throw new Error(`Unknown operator ${op}`); 47 | 48 | function stringOperator() { 49 | const pair = getStringPair(); 50 | if (op === operators.startsWithString) { 51 | if (!pair) return false; 52 | const [left, right] = pair; 53 | return left.startsWith(right); 54 | } 55 | if (op === operators.endsWithString) { 56 | if (!pair) return false; 57 | const [left, right] = pair; 58 | return left.endsWith(right); 59 | } 60 | if (op === operators.containsString) { 61 | if (!pair) return false; 62 | const [left, right] = pair; 63 | return left.includes(right); 64 | } 65 | } 66 | 67 | function numberOperator() { 68 | const pair = getNumberPair(); 69 | if (op === operators.greaterThanNumber) { 70 | if (!pair) return false; 71 | const [left, right] = pair; 72 | return left > right; 73 | } 74 | if (op === operators.lessThanNumber) { 75 | if (!pair) return false; 76 | const [left, right] = pair; 77 | return left < right; 78 | } 79 | if (op === operators.greaterThanOrEqualsNumber) { 80 | if (!pair) return false; 81 | const [left, right] = pair; 82 | return left >= right; 83 | } 84 | if (op === operators.lessThanOrEqualsNumber) { 85 | if (!pair) return false; 86 | const [left, right] = pair; 87 | return left <= right; 88 | } 89 | } 90 | 91 | function getNumberPair() { 92 | if (typeof left !== "number" || typeof right !== "number") { 93 | return false; 94 | } 95 | return [left, right]; 96 | } 97 | 98 | function getStringPair() { 99 | if (typeof left !== "string" || typeof right !== "string") { 100 | return false; 101 | } 102 | return [left, right]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@virtualstate/kdl` 2 | 3 | [KDL](https://github.com/kdl-org/kdl) Tooling for [JSX](https://github.com/virtualstate/focus) 4 | 5 | [//]: # (badges) 6 | 7 | ### Support 8 | 9 | ![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue) ![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue) 10 | 11 | ### Test Coverage 12 | 13 | ![96.54%25 lines covered](https://img.shields.io/badge/lines-96.54%25-brightgreen) ![96.54%25 statements covered](https://img.shields.io/badge/statements-96.54%25-brightgreen) ![93.24%25 functions covered](https://img.shields.io/badge/functions-93.24%25-brightgreen) ![90.54%25 branches covered](https://img.shields.io/badge/branches-90.54%25-brightgreen) 14 | 15 | [//]: # (badges) 16 | 17 | # Preparing queries 18 | 19 | Queries are not preformed as soon as they are created, but they are partially prepared. 20 | While the query runs, additional parts to the query will be included in as needed. 21 | 22 | To prepare a query for a JSX node, import and use `prepare` 23 | 24 | ```typescript jsx 25 | import {prepare} from "@virtualstate/kdl"; 26 | 27 | const node = ( 28 |
29 |

@virtualstate/focus

30 |
Version: 1.0.0
31 |
32 | ); 33 | ``` 34 | 35 | The first parameter, is the JSX node you want to query 36 | The second parameter, is a string containing [KDL Query Language](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md) 37 | 38 | ```typescript jsx 39 | const result = prepare( 40 | node, 41 | `main blockquote > span` 42 | ); 43 | ``` 44 | 45 | The result is an async object that can be resolved in many ways 46 | 47 | First, if used as a promise, will result in an array of matching JSX nodes 48 | 49 | ```typescript jsx 50 | const [span] = await result 51 | console.log(span); // Is the node for 1.0.0 52 | ``` 53 | 54 | If used as an async iterable, then snapshots of results can be accessed, allowing for earlier processing 55 | of earlier found JSX nodes 56 | 57 | ```typescript jsx 58 | for await (const [span] of result) { 59 | if (!span) continue; 60 | // We have at least one span! 61 | console.log(span) // Is the node for 1.0.0 62 | } 63 | ``` 64 | 65 | If used as an iterable, and destructuring is used, the individual destructured values will 66 | be async objects too, which can be used as a promise or async iterable 67 | 68 | ```typescript jsx 69 | const [firstSpan] = result; 70 | const span = await firstSpan; 71 | console.log(span) // Is the node for 1.0.0 72 | ``` 73 | ```typescript jsx 74 | const [firstSpan] = result; 75 | for await (const span of firstSpan) { 76 | console.log(span) // Is the node for 1.0.0 77 | } 78 | ``` 79 | 80 | The async object returned from prepare supports many array like operations, 81 | like `.at`, `.filter`, `.map`, `.group`, `.flatMap`, and [more](https://github.com/virtualstate/promise/blob/143b070e298b3417ac13b891b818d567c7346522/src/split/type.ts#L104-L138) 82 | 83 | These operations are performed on the individual snapshots yielded across the lifecycle of the query 84 | 85 | The map operator is also available, which can be used to directly return information about the node found 86 | 87 | ```typescript jsx 88 | const [value] = await prepare( 89 | node, 90 | `main blockquote > span => val()` 91 | ); 92 | 93 | console.log(value); // Logs the content of the span "1.0.0" 94 | ``` 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/tokenizer/tokens/query-accessor/index.ts: -------------------------------------------------------------------------------- 1 | import Chevrotain from "chevrotain"; 2 | import type { TokenType } from "chevrotain"; 3 | 4 | const { createToken } = Chevrotain; 5 | 6 | export const QueryAccessorParseTokens: TokenType[] = []; 7 | 8 | export const WhiteSpace = createToken({ 9 | name: "WhiteSpace", 10 | pattern: /[\s\n]+/, 11 | group: Chevrotain.Lexer.SKIPPED, 12 | }); 13 | QueryAccessorParseTokens.push(WhiteSpace); 14 | export const Raw = createToken({ 15 | name: "Raw", 16 | pattern: /r#".*"#/, 17 | line_breaks: true, 18 | }); 19 | QueryAccessorParseTokens.push(Raw); 20 | export const String = createToken({ 21 | name: "String", 22 | pattern: /"[^"]*"/, 23 | line_breaks: true, 24 | }); 25 | QueryAccessorParseTokens.push(String); 26 | export const Number = createToken({ 27 | name: "Number", 28 | pattern: /[+-]?\d+(?:\.\d+)?/, 29 | }); 30 | QueryAccessorParseTokens.push(Number); 31 | export const Boolean = createToken({ 32 | name: "Boolean", 33 | pattern: /true|false/, 34 | }); 35 | QueryAccessorParseTokens.push(Boolean); 36 | 37 | export const GetValue = createToken({ 38 | name: "GetValue", 39 | pattern: /val\((\d+)?\)/, 40 | }); 41 | QueryAccessorParseTokens.push(GetValue); 42 | export const GetTag = createToken({ 43 | name: "GetTag", 44 | pattern: /tag\(\)/, 45 | }); 46 | QueryAccessorParseTokens.push(GetTag); 47 | export const GetName = createToken({ 48 | name: "GetName", 49 | pattern: /name\(\)/, 50 | }); 51 | QueryAccessorParseTokens.push(GetName); 52 | export const GetProperty = createToken({ 53 | name: "GetProperty", 54 | pattern: /prop(?:\(([^)]+)?\))?/, 55 | }); 56 | QueryAccessorParseTokens.push(GetProperty); 57 | export const GetValues = createToken({ 58 | name: "GetValues", 59 | pattern: /values\(\)/, 60 | }); 61 | QueryAccessorParseTokens.push(GetValues); 62 | 63 | export const Selector = createToken({ 64 | name: "GetProperty", 65 | pattern: /[^\s=!><^$*]+/, 66 | }); 67 | QueryAccessorParseTokens.push(Selector); 68 | 69 | export const Equals = createToken({ 70 | name: "Equals", 71 | pattern: /=/, 72 | }); 73 | QueryAccessorParseTokens.push(Equals); 74 | export const NotEquals = createToken({ 75 | name: "NotEquals", 76 | pattern: /!=/, 77 | }); 78 | QueryAccessorParseTokens.push(NotEquals); 79 | 80 | export const GreaterThanOrEqual = createToken({ 81 | name: "GreaterThanOrEqual", 82 | pattern: />=/, 83 | }); 84 | QueryAccessorParseTokens.push(GreaterThanOrEqual); 85 | export const LessThanOrEqual = createToken({ 86 | name: "LessThanOrEqual", 87 | pattern: /<=/, 88 | }); 89 | QueryAccessorParseTokens.push(LessThanOrEqual); 90 | export const GreaterThan = createToken({ 91 | name: "GreaterThan", 92 | pattern: />/, 93 | }); 94 | QueryAccessorParseTokens.push(GreaterThan); 95 | export const LessThan = createToken({ 96 | name: "LessThan", 97 | pattern: / 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

Paragraph 1

25 |

Paragraph 2

26 |

Paragraph 3

27 |
28 | 29 | ); 30 | 31 | { 32 | const query = prepare( 33 | root, 34 | `input[type="number"][value >= 1][value <= 2] || input[type="checkbox"][checked] || another input || another top() deep input` 35 | ); 36 | 37 | console.log({ 38 | query, 39 | }); 40 | 41 | let snapshot; 42 | 43 | for await (snapshot of query) { 44 | console.log("snapshot", snapshot); 45 | } 46 | console.log("final", snapshot); 47 | } 48 | 49 | { 50 | const query = prepare(root, `another > tree input[type="number"][value=2]`); 51 | 52 | let snapshot; 53 | for await (snapshot of query) { 54 | console.log("snapshot", snapshot); 55 | } 56 | console.log("final", snapshot); 57 | } 58 | 59 | { 60 | const query = prepare( 61 | root, 62 | `another + deep tree > jump input[type="checkbox"]` 63 | ); 64 | 65 | let snapshot; 66 | for await (snapshot of query) { 67 | console.log("snapshot", snapshot); 68 | } 69 | console.log("final", snapshot); 70 | } 71 | 72 | { 73 | const query = prepare(root, `input ~ input`); 74 | 75 | let snapshot; 76 | for await (snapshot of query) { 77 | console.log("snapshot", snapshot); 78 | } 79 | console.log("final", snapshot); 80 | } 81 | 82 | { 83 | const query = prepare(root, `p[first] ~ p[last]`); 84 | 85 | let snapshot; 86 | for await (snapshot of query) { 87 | console.log("snapshot", snapshot); 88 | } 89 | console.log("final", snapshot); 90 | } 91 | 92 | { 93 | const query = prepare(root, `[] > p[first] ~ p[last]`); 94 | 95 | let snapshot; 96 | for await (snapshot of query) { 97 | console.log("snapshot", snapshot); 98 | } 99 | console.log("final", snapshot); 100 | } 101 | 102 | { 103 | const query = prepare( 104 | PackageTree, 105 | `top() > package dependencies[prop(platform) != r#"windows"#] || top() > package dependencies[prop(platform) = "windows"]` 106 | ); 107 | 108 | const result = await query; 109 | console.log(result); 110 | ok(result); 111 | ok(result.length === 2); 112 | ok(properties(result[0]).platform !== "windows"); 113 | ok(properties(result[1]).platform === "windows"); 114 | } 115 | 116 | { 117 | const query = prepare( 118 | PackageTree, 119 | `dependencies[prop(platform) ^= "win"] top() package > version` 120 | ); 121 | 122 | const result = await query; 123 | console.log(result); 124 | ok(result); 125 | ok(result.length === 1); 126 | ok(name(result[0]) === "version"); 127 | } 128 | { 129 | const query = prepare(PackageTree, `lastBuiltAt[val() > 0]`); 130 | 131 | const result = await query; 132 | console.log(result); 133 | ok(result); 134 | ok(result.length === 1); 135 | ok(name(result[0]) === "lastBuiltAt"); 136 | } 137 | 138 | 139 | { 140 | const query = prepare(PackageTree, `lastBuiltAt => name()`); 141 | 142 | const result = await query; 143 | console.log(result); 144 | ok(result); 145 | ok(result.length === 1); 146 | ok(result[0] === "lastBuiltAt"); 147 | } 148 | 149 | { 150 | const query = prepare(PackageTree, `lastBuiltAt => (name(), val())`); 151 | 152 | const result = await query; 153 | console.log(result); 154 | ok(result); 155 | ok(result.length === 1); 156 | ok(Array.isArray(result[0])); 157 | ok(result[0][0] === "lastBuiltAt"); 158 | ok(result[0][1] === 12345); 159 | } 160 | 161 | { 162 | const [last] = await prepare(root, `p[first] ~ p[last] => last`); 163 | console.log(last); 164 | ok(last === true); 165 | } 166 | -------------------------------------------------------------------------------- /src/tests/package.tsx: -------------------------------------------------------------------------------- 1 | import { toKDLString } from "../string"; 2 | import { h, named } from "../static-h"; 3 | import * as jsx from "@virtualstate/focus"; 4 | import { rawKDLQuery } from "../query"; 5 | 6 | const Package = named("package"); 7 | 8 | export const PackageTree = ( 9 | 10 | foo 11 | 1.0.0 12 | 13 | 1.0.0 14 | 15 | 16 | 2.0.0 17 | 18 | {12345} 19 | 20 | ); 21 | 22 | console.log(await jsx.children(PackageTree)); 23 | 24 | console.log(await toKDLString(PackageTree)); 25 | 26 | for await (const string of toKDLString(PackageTree)) { 27 | console.log(string); 28 | } 29 | 30 | const multiTree = { 31 | source: "name", 32 | options: { 33 | attribute: "value", 34 | value: 1, 35 | }, 36 | children: [ 37 | { 38 | type: "main", 39 | children: [ 40 | { 41 | $$type: "section", 42 | props: { 43 | id: "main-section", 44 | }, 45 | children: { 46 | async *[Symbol.asyncIterator]() { 47 | yield [ 48 | { 49 | type: "h1", 50 | children: ["hello", "world"], 51 | }, 52 | "whats up", 53 | ]; 54 | }, 55 | }, 56 | }, 57 | ], 58 | }, 59 | ], 60 | }; 61 | 62 | for await (const string of toKDLString(multiTree)) { 63 | console.log(string); 64 | } 65 | 66 | const queries = [ 67 | `top() > package dependencies[prop(platform) = "windows"]`, 68 | `top() > package dependencies[prop(platform) != "windows"]`, 69 | `top() > package dependencies[prop(platform) != r#"windows"#] || top() > package dependencies[prop(platform) = "windows"]`, 70 | `dependencies`, 71 | `dependencies[prop(platform) ^= "win"]`, 72 | `dependencies[prop(platform) $= "s"]`, 73 | `dependencies[prop(platform) *= "in"]`, 74 | `dependencies[prop(platform)]`, 75 | `dependencies[platform]`, 76 | `dependencies[platform = "windows"]`, 77 | `lastBuiltAt[val() > 0]`, 78 | `lastBuiltAt[val() >= 0]`, 79 | `lastBuiltAt[val() < 9999999]`, 80 | `lastBuiltAt[val() <= 9999999]`, 81 | `dependencies + dependencies`, 82 | `version ~ dependencies`, 83 | `dependencies[prop(platform) ^= "win"] top() package > version`, 84 | ]; 85 | 86 | const Query = rawKDLQuery`top() > package dependencies[prop(platform) = "windows"]`; 87 | const result = { 88 | name: Symbol.for(":kdl/fragment"), 89 | children: { 90 | [Symbol.asyncIterator]() { 91 | return Query({}, PackageTree)[Symbol.asyncIterator](); 92 | }, 93 | }, 94 | }; 95 | console.log(await toKDLString(result)); 96 | 97 | for (const query of queries) { 98 | const Query3 = rawKDLQuery(query); 99 | const result3 = { 100 | name: Symbol.for(":kdl/fragment"), 101 | children: { 102 | [Symbol.asyncIterator]() { 103 | return Query3({}, PackageTree)[Symbol.asyncIterator](); 104 | }, 105 | }, 106 | }; 107 | console.log(await toKDLString(result3)); 108 | } 109 | const Query4 = rawKDLQuery`section`; 110 | const result4 = { 111 | name: Symbol.for(":kdl/fragment"), 112 | children: { 113 | [Symbol.asyncIterator]() { 114 | return Query4({}, multiTree)[Symbol.asyncIterator](); 115 | }, 116 | }, 117 | }; 118 | console.log(await toKDLString(result4)); 119 | 120 | const Query5 = rawKDLQuery`section[prop(id) = "main-section"] h1`; 121 | const result5 = { 122 | name: Symbol.for(":kdl/fragment"), 123 | children: { 124 | [Symbol.asyncIterator]() { 125 | return Query5({}, multiTree)[Symbol.asyncIterator](); 126 | }, 127 | }, 128 | }; 129 | console.log(await toKDLString(result5)); 130 | 131 | console.log( 132 | await toKDLString({ 133 | name: Symbol.for(":kdl/fragment"), 134 | children: [ 135 | { 136 | name: "head", 137 | children: [ 138 | { 139 | name: "title", 140 | values: ["Website"], 141 | }, 142 | ], 143 | }, 144 | { 145 | name: "body", 146 | children: ["hello"], 147 | }, 148 | ], 149 | }) 150 | ); 151 | -------------------------------------------------------------------------------- /src/tests/resources/cases/package.kdl: -------------------------------------------------------------------------------- 1 | queries name="package" { 2 | input { 3 | package { 4 | name "foo" 5 | version "1.0.0" 6 | dependencies platform="windows" { 7 | winapi "1.0.0" path="./crates/my-winapi-fork" 8 | } 9 | dependencies { 10 | miette "2.0.0" dev=true 11 | } 12 | lastBuiltAt 12345 13 | } 14 | } 15 | query "top() > package dependencies[prop(platform) = \"windows\"]" index=0 { 16 | output { 17 | dependencies platform="windows" { 18 | winapi "1.0.0" path="./crates/my-winapi-fork" 19 | } 20 | } 21 | "@virtualstate/kdl/output/match" true 22 | } 23 | query "top() > package dependencies[prop(platform) != \"windows\"]" index=1 { 24 | output { 25 | dependencies { 26 | miette "2.0.0" dev=true 27 | } 28 | } 29 | "@virtualstate/kdl/output/match" true 30 | } 31 | query "top() > package dependencies[prop(platform) != r#\"windows\"#] || top() > package dependencies[prop(platform) = \"windows\"]" index=2 { 32 | output { 33 | dependencies { 34 | miette "2.0.0" dev=true 35 | } 36 | dependencies platform="windows" { 37 | winapi "1.0.0" path="./crates/my-winapi-fork" 38 | } 39 | } 40 | "@virtualstate/kdl/output/match" true 41 | } 42 | query "dependencies" index=3 { 43 | output { 44 | dependencies platform="windows" { 45 | winapi "1.0.0" path="./crates/my-winapi-fork" 46 | } 47 | dependencies { 48 | miette "2.0.0" dev=true 49 | } 50 | } 51 | "@virtualstate/kdl/output/match" true 52 | } 53 | query "dependencies[prop(platform) ^= \"win\"]" index=4 { 54 | output { 55 | dependencies platform="windows" { 56 | winapi "1.0.0" path="./crates/my-winapi-fork" 57 | } 58 | } 59 | "@virtualstate/kdl/output/match" true 60 | } 61 | query "dependencies[prop(platform) $= \"s\"]" index=5 { 62 | output { 63 | dependencies platform="windows" { 64 | winapi "1.0.0" path="./crates/my-winapi-fork" 65 | } 66 | } 67 | "@virtualstate/kdl/output/match" true 68 | } 69 | query "dependencies[prop(platform) *= \"in\"]" index=6 { 70 | output { 71 | dependencies platform="windows" { 72 | winapi "1.0.0" path="./crates/my-winapi-fork" 73 | } 74 | } 75 | "@virtualstate/kdl/output/match" true 76 | } 77 | query "dependencies[prop(platform)]" index=7 { 78 | output { 79 | dependencies platform="windows" { 80 | winapi "1.0.0" path="./crates/my-winapi-fork" 81 | } 82 | } 83 | "@virtualstate/kdl/output/match" true 84 | } 85 | query "dependencies[platform]" index=8 { 86 | output { 87 | dependencies platform="windows" { 88 | winapi "1.0.0" path="./crates/my-winapi-fork" 89 | } 90 | } 91 | "@virtualstate/kdl/output/match" true 92 | } 93 | query "dependencies[platform = \"windows\"]" index=9 { 94 | output { 95 | dependencies platform="windows" { 96 | winapi "1.0.0" path="./crates/my-winapi-fork" 97 | } 98 | } 99 | "@virtualstate/kdl/output/match" true 100 | } 101 | query "lastBuiltAt[val() > 0]" index=10 { 102 | output { 103 | lastBuiltAt 12345 104 | } 105 | "@virtualstate/kdl/output/match" true 106 | } 107 | query "lastBuiltAt[val() >= 0]" index=11 { 108 | output { 109 | lastBuiltAt 12345 110 | } 111 | "@virtualstate/kdl/output/match" true 112 | } 113 | query "lastBuiltAt[val() < 9999999]" index=12 { 114 | output { 115 | lastBuiltAt 12345 116 | } 117 | "@virtualstate/kdl/output/match" true 118 | } 119 | query "lastBuiltAt[val() <= 9999999]" index=13 { 120 | output { 121 | lastBuiltAt 12345 122 | } 123 | "@virtualstate/kdl/output/match" true 124 | } 125 | query "dependencies + dependencies" index=14 { 126 | output { 127 | dependencies { 128 | miette "2.0.0" dev=true 129 | } 130 | } 131 | "@virtualstate/kdl/output/match" true 132 | } 133 | query "version ~ dependencies" index=15 { 134 | output { 135 | dependencies platform="windows" { 136 | winapi "1.0.0" path="./crates/my-winapi-fork" 137 | } 138 | dependencies { 139 | miette "2.0.0" dev=true 140 | } 141 | } 142 | "@virtualstate/kdl/output/match" true 143 | } 144 | query "dependencies[prop(platform) ^= \"win\"] top() package > version" index=16 { 145 | output { 146 | version "1.0.0" 147 | } 148 | "@virtualstate/kdl/output/match" true 149 | } 150 | query "version" index=17 { 151 | output { 152 | version "1.0.0" 153 | } 154 | "@virtualstate/kdl/output/match" true 155 | } 156 | query "name" index=18 { 157 | output { 158 | name "foo" 159 | } 160 | "@virtualstate/kdl/output/match" true 161 | } 162 | query "lastBuiltAt[val()]" index=19 { 163 | output { 164 | lastBuiltAt 12345 165 | } 166 | "@virtualstate/kdl/output/match" true 167 | } 168 | } -------------------------------------------------------------------------------- /src/tests/resources/generate.tsx: -------------------------------------------------------------------------------- 1 | import * as Setup from "./setup"; 2 | import { toKDLString } from "../../string"; 3 | import { dirname, join } from "node:path"; 4 | import { h, createFragment, named } from "../../static-h"; 5 | import { runKDLQuery } from "../../query"; 6 | import { promises as fs } from "node:fs"; 7 | import * as jsx from "@virtualstate/focus"; 8 | 9 | type SetupKey = keyof typeof Setup; 10 | type SuffixSetupKey = SetupKey & `${string}${Suffix}`; 11 | 12 | const documentKeys = Object.keys(Setup).filter(isDocumentKey); 13 | 14 | const queriesSuffix = "Queries" as const; 15 | const outputsSuffix = "Outputs" as const; 16 | const optionsSuffix = "Options" as const; 17 | 18 | const { pathname } = new URL(import.meta.url); 19 | const directory = dirname(pathname); 20 | const targetDirectory = join(directory, "cases"); 21 | const [buildDirectoryName] = directory 22 | .replace(process.cwd(), "") 23 | .split("/") 24 | .filter(Boolean); 25 | const buildDirectory = join(process.cwd(), buildDirectoryName); 26 | const srcDirectory = join(process.cwd(), "src"); 27 | const srcTargetDirectory = targetDirectory.replace( 28 | buildDirectory, 29 | srcDirectory 30 | ); 31 | 32 | await fs.mkdir(targetDirectory).catch((error) => void error); 33 | await fs.mkdir(srcTargetDirectory).catch((error) => void error); 34 | 35 | const RuntimeOutput = named("@virtualstate/kdl/output/match"); 36 | 37 | const baseOptions = {}; 38 | 39 | for (const documentKey of documentKeys) { 40 | const prefix = documentKey.replace(/Document$/, ""); 41 | 42 | const document = Setup[documentKey]; 43 | const queriesKey = `${prefix}${queriesSuffix}` as const; 44 | const outputsKey = `${prefix}${outputsSuffix}` as const; 45 | const optionsKey = `${prefix}${optionsSuffix}` as const; 46 | const queries: ReadonlyArray = isSpecificKey( 47 | queriesKey, 48 | queriesSuffix 49 | ) 50 | ? Setup[queriesKey] 51 | : []; 52 | const outputs: ReadonlyArray = ( 53 | isSpecificKey(outputsKey, outputsSuffix) ? Setup[outputsKey] : [] 54 | ).filter(jsx.isUnknownJSXNode); 55 | const options: Record = { 56 | ...baseOptions, 57 | ...(isSpecificKey(optionsKey, optionsSuffix) ? Setup[optionsKey] : {}), 58 | }; 59 | 60 | console.log(options); 61 | 62 | jsx.ok( 63 | queries.length === outputs.length, 64 | `Expected query count to match output count for ${prefix}` 65 | ); 66 | jsx.ok(queries.length, "Expected at least one query"); 67 | 68 | const received = await Promise.all( 69 | queries.map(async (query) => { 70 | return runKDLQuery(query, document); 71 | }) 72 | ); 73 | 74 | const runtimeOutputs = await Promise.all( 75 | queries.map(async (query, index): Promise<[string, string]> => { 76 | const expectedOutput = await toKDLString(outputs[index], options); 77 | const receivedOutput = await toKDLString(received[index], options); 78 | return [expectedOutput, receivedOutput]; 79 | }) 80 | ); 81 | 82 | const runtimeOutputMatch = runtimeOutputs.map(([left, right]) => { 83 | return left === right; 84 | }); 85 | 86 | const output = ( 87 | 88 | {document} 89 | {queries.map((query, index) => { 90 | return ( 91 | 92 | {query} 93 | {outputs[index]} 94 | {!runtimeOutputMatch[index] ? ( 95 | {received[index]} 96 | ) : undefined} 97 | {!runtimeOutputMatch[index] ? ( 98 | {runtimeOutputs[index]} 99 | ) : undefined} 100 | {runtimeOutputMatch[index]} 101 | 102 | ); 103 | })} 104 | 105 | ); 106 | 107 | const outputString = await toKDLString(output, options); 108 | 109 | // console.log(outputString) 110 | 111 | await fs.writeFile( 112 | join(targetDirectory, `${prefix}.kdl`), 113 | outputString, 114 | "utf-8" 115 | ); 116 | await fs.writeFile( 117 | join(srcTargetDirectory, `${prefix}.kdl`), 118 | outputString, 119 | "utf-8" 120 | ); 121 | 122 | // const runtimeNotMatchingIndex = runtimeOutputMatch 123 | // .map((match, index) => match ? -1 : index) 124 | // .find(value => value > -1); 125 | // 126 | // if (typeof runtimeNotMatchingIndex === "number") { 127 | // throw new Error(`Output does not match for ${queries[runtimeNotMatchingIndex]}`); 128 | // } 129 | } 130 | 131 | function isDocumentKey(key: string): key is SuffixSetupKey<"Document"> { 132 | return isSpecificKey(key, "Document"); 133 | } 134 | 135 | function isSpecificKey( 136 | key: string, 137 | suffix: Suffix 138 | ): key is SuffixSetupKey { 139 | return isKey(key) && key.endsWith(suffix); 140 | } 141 | 142 | function isKey(key: string): key is SetupKey { 143 | return jsx.isLike(key) && !!Setup[key]; 144 | } 145 | -------------------------------------------------------------------------------- /scripts/post-build.js: -------------------------------------------------------------------------------- 1 | import "./correct-import-extensions.js"; 2 | import { promises as fs } from "fs"; 3 | import { rollup } from "rollup"; 4 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 5 | import ignore from "rollup-plugin-ignore"; 6 | import babel from "rollup-plugin-babel"; 7 | import { dirname, resolve } from "path"; 8 | 9 | const { pathname } = new URL(import.meta.url); 10 | const cwd = resolve(dirname(pathname), ".."); 11 | 12 | { 13 | // /Volumes/Extreme/Users/fabian/src/virtualstate/esnext/tests/app-history.playwright.wpt.js 14 | // /Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js 15 | 16 | // console.log({ 17 | // cwd, 18 | // path: 19 | // `/Volumes/Extreme/Users/fabian/src/virtualstate/app-history/esnext/tests/app-history.playwright.wpt.js` === 20 | // `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 21 | // p: `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 22 | // }); 23 | 24 | // const bundle = await rollup({ 25 | // input: "./esnext/tests/index.js", 26 | // plugins: [ 27 | // ignore([ 28 | // "playwright", 29 | // "fs", 30 | // "path", 31 | // "uuid", 32 | // "cheerio", 33 | // "@virtualstate/app-history", 34 | // "@virtualstate/app-history-imported", 35 | // `${cwd}/esnext/tests/app-history.playwright.js`, 36 | // `${cwd}/esnext/tests/app-history.playwright.wpt.js`, 37 | // `${cwd}/esnext/tests/dependencies-input.js`, 38 | // `${cwd}/esnext/tests/dependencies.js`, 39 | // "./app-history.playwright.js", 40 | // "./app-history.playwright.wpt.js", 41 | // ]), 42 | // nodeResolve(), 43 | // ], 44 | // inlineDynamicImports: true, 45 | // treeshake: { 46 | // preset: "smallest", 47 | // moduleSideEffects: "no-external", 48 | // }, 49 | // }); 50 | // await bundle.write({ 51 | // sourcemap: true, 52 | // output: { 53 | // file: "./esnext/tests/rollup.js", 54 | // }, 55 | // inlineDynamicImports: true, 56 | // format: "cjs", 57 | // interop: "auto", 58 | // globals: { 59 | // "esnext/tests/app-history.playwright.js": "globalThis", 60 | // }, 61 | // }); 62 | } 63 | 64 | if (!process.env.NO_COVERAGE_BADGE_UPDATE) { 65 | const badges = []; 66 | 67 | const { name } = await fs.readFile("package.json", "utf-8").then(JSON.parse); 68 | 69 | badges.push( 70 | "### Support\n\n", 71 | "![Node.js supported](https://img.shields.io/badge/node-%3E%3D16.0.0-blue)", 72 | "![Deno supported](https://img.shields.io/badge/deno-%3E%3D1.17.0-blue)", 73 | // "![Chromium supported](https://img.shields.io/badge/chromium-%3E%3D98.0.4695.0-blue)", 74 | // "![Webkit supported](https://img.shields.io/badge/webkit-%3E%3D15.4-blue)", 75 | // "![Firefox supported](https://img.shields.io/badge/firefox-%3E%3D94.0.1-blue)" 76 | ); 77 | 78 | badges.push( 79 | "\n\n### Test Coverage\n\n" 80 | // `![nycrc config on GitHub](https://img.shields.io/nycrc/${name.replace(/^@/, "")})` 81 | ); 82 | 83 | // const wptResults = await fs 84 | // .readFile("coverage/wpt.results.json", "utf8") 85 | // .then(JSON.parse) 86 | // .catch(() => ({})); 87 | // if (wptResults?.total) { 88 | // const message = `${wptResults.pass}/${wptResults.total}`; 89 | // const name = "Web Platform Tests"; 90 | // badges.push( 91 | // `![${name} ${message}](https://img.shields.io/badge/${encodeURIComponent( 92 | // name 93 | // )}-${encodeURIComponent(message)}-brightgreen)` 94 | // ); 95 | // } 96 | 97 | const coverage = await fs 98 | .readFile("coverage/coverage-summary.json", "utf8") 99 | .then(JSON.parse) 100 | .catch(() => ({})); 101 | const coverageConfig = await fs.readFile(".nycrc", "utf8").then(JSON.parse); 102 | for (const [name, { pct }] of Object.entries(coverage?.total ?? {})) { 103 | const good = coverageConfig[name]; 104 | if (!good) continue; // not configured 105 | const color = pct >= good ? "brightgreen" : "yellow"; 106 | const message = `${pct}%25`; 107 | badges.push( 108 | `![${message} ${name} covered](https://img.shields.io/badge/${name}-${message}-${color})` 109 | ); 110 | } 111 | 112 | const tag = "[//]: # (badges)"; 113 | 114 | const readMe = await fs.readFile("README.md", "utf8"); 115 | const badgeStart = readMe.indexOf(tag); 116 | const badgeStartAfter = badgeStart + tag.length; 117 | if (badgeStart === -1) { 118 | throw new Error(`Expected to find "${tag}" in README.md`); 119 | } 120 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 121 | const badgeEndAfter = badgeEnd + tag.length; 122 | const readMeBefore = readMe.slice(0, badgeStart); 123 | const readMeAfter = readMe.slice(badgeEndAfter); 124 | 125 | const readMeNext = `${readMeBefore}${tag}\n\n${badges.join( 126 | " " 127 | )}\n\n${tag}${readMeAfter}`; 128 | await fs.writeFile("README.md", readMeNext); 129 | console.log("Wrote coverage badges!"); 130 | } 131 | -------------------------------------------------------------------------------- /src/string.ts: -------------------------------------------------------------------------------- 1 | import { union } from "@virtualstate/union"; 2 | import { anAsyncThing, TheAsyncThing } from "@virtualstate/promise/the-thing"; 3 | import * as jsx from "@virtualstate/focus"; 4 | import { isUnknownJSXNode } from "@virtualstate/focus"; 5 | 6 | /** 7 | * @experimental 8 | */ 9 | export const ResolveObjectAttributes = Symbol.for( 10 | "@virtualstate/kdl/resolveObjectAttributes" 11 | ); 12 | 13 | export interface ToKDLStringOptions { 14 | /** 15 | * @experimental 16 | */ 17 | [ResolveObjectAttributes]?: boolean; 18 | } 19 | 20 | /* 21 | import { KDL } from "@virtualstate/focus"; 22 | // The returned type from KDL.ToString is currently only for IDE debugging, it may not match the runtime output completely 23 | // I hope to expand it further to include all possible variations & then build in tests to match types to the implementation 24 | export function toKDLString(input: N): TheAsyncThing extends ("" | never) ? string : KDL.ToString> 25 | */ 26 | export function toKDLString( 27 | input: unknown, 28 | options?: ToKDLStringOptions 29 | ): TheAsyncThing; 30 | export function toKDLString( 31 | input: unknown, 32 | options?: unknown 33 | ): TheAsyncThing; 34 | export function toKDLString( 35 | input: unknown, 36 | options?: unknown 37 | ): TheAsyncThing { 38 | return anAsyncThing(toKDLStringInternal(input, options)); 39 | } 40 | 41 | async function* toKDLStringInternal( 42 | input: unknown, 43 | options?: unknown 44 | ): AsyncIterable { 45 | let yielded = false; 46 | // const { 47 | // name: nameInput, 48 | // props, 49 | // children, 50 | // values 51 | // } = inputNode; 52 | const inputNode = input; 53 | const nameInput = jsx.name(input); 54 | const values = jsx.values(input); 55 | const props = jsx.properties(input); 56 | 57 | // console.log({ nameInput, values, props }); 58 | 59 | let name: string; 60 | const nameInputOrEmpty = nameInput ?? ""; 61 | 62 | if (typeof nameInputOrEmpty === "string") { 63 | name = nameInputOrEmpty; 64 | if (!name) { 65 | name = ""; 66 | } 67 | if (!name || /[^a-z0-9]/i.test(name)) { 68 | name = JSON.stringify(name); 69 | } 70 | } else { 71 | name = nameInputOrEmpty.toString(); // Non-standard symbol, for internal process documents 72 | } 73 | 74 | const valuesString = [...values] 75 | .filter(jsx.isStaticChildNode) 76 | .map((value) => JSON.stringify(value)) 77 | .join(" "); 78 | 79 | const propsEntries = Object.entries(props); 80 | 81 | const propsObjects = propsEntries.filter(([, value]) => 82 | isUnknownJSXNode(value) 83 | ); 84 | const propsObjectsStrings = new Map( 85 | option(ResolveObjectAttributes) && propsObjects.length 86 | ? await Promise.all( 87 | propsObjects.map( 88 | async ([key, object]) => 89 | [key, `{${(await toKDLString(object)).trim()}}`] as const 90 | ) 91 | ) 92 | : [] 93 | ); 94 | 95 | const propsString = Object.keys(props) 96 | .map( 97 | (key) => 98 | `${key}=${propsObjectsStrings.get(key) ?? JSON.stringify(props[key])}` 99 | ) 100 | .join(" "); 101 | 102 | for await (const childrenSnapshot of jsx.children(input)) { 103 | // console.log({ childrenSnapshot }); 104 | yield* toStringChildren(childrenSnapshot); 105 | } 106 | 107 | if (yielded) return; 108 | yield `${`${name} ${valuesString}`.trim()} ${propsString}`; 109 | 110 | async function* toStringChildren(children: unknown[]) { 111 | const withoutUndefined = children.filter( 112 | (node) => jsx.isStaticChildNode(node) || node 113 | ); 114 | if (!withoutUndefined.length) return; 115 | 116 | const staticChildren = withoutUndefined.filter(jsx.isStaticChildNode); 117 | const staticChildrenStrings = [ 118 | ...valuesString, 119 | ...staticChildren.map((value) => JSON.stringify(value)), 120 | ]; 121 | const nonStaticChildren = 122 | staticChildren.length === withoutUndefined.length 123 | ? [] 124 | : children.filter((node) => !jsx.isStaticChildNode(node)); 125 | 126 | if (!nonStaticChildren.length) { 127 | yield withDetails([]); 128 | yielded = true; 129 | } else { 130 | for await (const strings of union( 131 | nonStaticChildren.map(toStringChildNode) 132 | )) { 133 | const withoutEmpty = strings.filter((value) => value); 134 | if (withoutEmpty.length) { 135 | yield withDetails(strings); 136 | yielded = true; 137 | } 138 | } 139 | } 140 | 141 | function withDetails(childrenStrings: string[]) { 142 | const childrenStringsFiltered = childrenStrings.filter((value) => value); 143 | const padding = jsx.isFragment(inputNode) ? "" : " "; 144 | const childrenStringBody = childrenStringsFiltered 145 | .map((value) => padStartLines(padding, value)) 146 | .join("\n"); 147 | if (jsx.isFragment(inputNode)) return childrenStringBody; 148 | const childrenString = childrenStringsFiltered.length 149 | ? ` {\n${childrenStringBody}\n}` 150 | : ""; 151 | return `${`${`${name} ${staticChildrenStrings.join( 152 | " " 153 | )}`.trim()} ${propsString}`.trim()}${childrenString}`; 154 | } 155 | } 156 | 157 | async function* toStringChildNode(node: unknown): AsyncIterable { 158 | if (!node) return; 159 | yield* toKDLStringInternal(node, options); 160 | } 161 | 162 | function padStartLines(padding: string, value: string) { 163 | return (value ?? "") 164 | .split("\n") 165 | .map((line) => `${padding}${line}`) 166 | .join("\n"); 167 | } 168 | 169 | function option(name: string | symbol) { 170 | if (!isOptions(options)) return undefined; 171 | return options[name]; 172 | 173 | function isOptions(value: unknown): value is Record { 174 | return typeof options === "object" && !!options; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/tokenizer/index.ts: -------------------------------------------------------------------------------- 1 | import Chevrotain from "chevrotain"; 2 | import { 3 | QueryParseTokens, 4 | Query, 5 | QueryAccessorParseTokens, 6 | QueryAccessor, 7 | } from "./tokens"; 8 | import { isLike } from "@virtualstate/focus"; 9 | 10 | export { Query, QueryAccessor }; 11 | 12 | export interface SelectorToken { 13 | type: "Selector"; 14 | text: string; 15 | image: string; 16 | } 17 | 18 | export interface WhiteSpaceToken { 19 | type: "WhiteSpace"; 20 | text: string; 21 | image: string; 22 | } 23 | 24 | export interface SeparatorToken { 25 | type: "Separator"; 26 | text: string; 27 | image: string; 28 | } 29 | 30 | export interface MapToken { 31 | type: "Map"; 32 | text: string; 33 | image: string; 34 | } 35 | 36 | export interface AccessorToken { 37 | type: "Accessor"; 38 | text: string; 39 | accessor: string; 40 | left?: QueryToken; 41 | right?: QueryToken; 42 | operator?: string; 43 | image: string; 44 | } 45 | 46 | export interface MapAccessorToken { 47 | type: "MapAccessor"; 48 | text: string; 49 | accessors: AccessorToken[]; 50 | } 51 | 52 | export interface BooleanToken { 53 | type: "Boolean"; 54 | boolean: boolean; 55 | image: string; 56 | } 57 | 58 | export interface StringToken { 59 | type: "String"; 60 | text: string; 61 | image: string; 62 | } 63 | 64 | export interface NumberToken { 65 | type: "Number"; 66 | number: number; 67 | image: string; 68 | } 69 | 70 | export interface GetTagToken { 71 | type: "GetTag"; 72 | image: string; 73 | } 74 | 75 | export interface GetNameToken { 76 | type: "GetName"; 77 | image: string; 78 | } 79 | 80 | export interface GetPropertyToken { 81 | type: "GetProperty"; 82 | text: string; 83 | image: string; 84 | } 85 | 86 | export interface GetValueToken { 87 | type: "GetValue"; 88 | index?: number; 89 | image: string; 90 | } 91 | 92 | export interface GetValuesToken { 93 | type: "GetValues"; 94 | image: string; 95 | } 96 | 97 | export type QueryValueToken = BooleanToken | NumberToken | StringToken; 98 | 99 | export interface TagToken { 100 | type: "Tag"; 101 | text: string; 102 | tag: string; 103 | image: string; 104 | } 105 | 106 | export interface DirectChildToken { 107 | type: "DirectChild"; 108 | image: string; 109 | } 110 | 111 | export interface TopToken { 112 | type: "Top"; 113 | image: string; 114 | } 115 | 116 | export interface FollowsToken { 117 | type: "Follows"; 118 | image: string; 119 | } 120 | 121 | export interface ImmediatelyFollowsToken { 122 | type: "ImmediatelyFollows"; 123 | image: string; 124 | } 125 | 126 | export interface OrToken { 127 | type: "Or"; 128 | image: string; 129 | } 130 | 131 | export interface GenericToken extends Record { 132 | type: string; 133 | text: string; 134 | image: string; 135 | } 136 | 137 | export type QueryToken = 138 | | SelectorToken 139 | | AccessorToken 140 | | QueryValueToken 141 | | TagToken 142 | | GetTagToken 143 | | GetNameToken 144 | | GetValuesToken 145 | | GetValueToken 146 | | GetPropertyToken 147 | | DirectChildToken 148 | | TopToken 149 | | FollowsToken 150 | | ImmediatelyFollowsToken 151 | | OrToken 152 | | WhiteSpaceToken 153 | | SeparatorToken 154 | | MapToken 155 | | MapAccessorToken 156 | | GenericToken; 157 | 158 | export function isSelectorToken(token: QueryToken): token is SelectorToken { 159 | return token.type === "Selector"; 160 | } 161 | 162 | export function isAccessorToken(token: QueryToken): token is AccessorToken { 163 | return token.type === "Accessor"; 164 | } 165 | 166 | export function isStringToken(token: QueryToken): token is StringToken { 167 | return token.type === "String"; 168 | } 169 | 170 | export function isNumberToken(token: QueryToken): token is NumberToken { 171 | return token.type === "Number"; 172 | } 173 | 174 | export function isBooleanToken(token: QueryToken): token is BooleanToken { 175 | return token.type === "Boolean"; 176 | } 177 | 178 | export function isQueryValueToken(token: QueryToken): token is QueryValueToken { 179 | return isStringToken(token) || isNumberToken(token) || isBooleanToken(token); 180 | } 181 | 182 | export function isTagToken(token: QueryToken): token is TagToken { 183 | return token.type === "Tag"; 184 | } 185 | 186 | export function isGetTagToken(token: QueryToken): token is GetTagToken { 187 | return token.type === "GetTag"; 188 | } 189 | 190 | export function isGetNameToken(token: QueryToken): token is GetNameToken { 191 | return token.type === "GetName"; 192 | } 193 | 194 | export function isGetPropertyToken( 195 | token: QueryToken 196 | ): token is GetPropertyToken { 197 | return token.type === "GetProperty"; 198 | } 199 | 200 | export function isGetValueToken(token: QueryToken): token is GetValueToken { 201 | return token.type === "GetValue"; 202 | } 203 | 204 | export function isGetValuesToken(token: QueryToken): token is GetValuesToken { 205 | return token.type === "GetValues"; 206 | } 207 | 208 | export function isTopToken(token: QueryToken): token is TopToken { 209 | return token.type === "Top"; 210 | } 211 | 212 | export function isDirectChildToken( 213 | token: QueryToken 214 | ): token is DirectChildToken { 215 | return token.type === "DirectChild"; 216 | } 217 | 218 | export function isFollowsToken(token: QueryToken): token is FollowsToken { 219 | return token.type === "Follows"; 220 | } 221 | 222 | export function isImmediatelyFollowsToken( 223 | token: QueryToken 224 | ): token is ImmediatelyFollowsToken { 225 | return token.type === "ImmediatelyFollows"; 226 | } 227 | 228 | export function isOrToken(token: QueryToken): token is OrToken { 229 | return token.type === "Or"; 230 | } 231 | 232 | export function isQueryToken(value: unknown): value is QueryToken { 233 | return isLike<{ type: unknown }>(value) && typeof value.type === "string"; 234 | } 235 | 236 | export function isWhiteSpaceToken(token: unknown): token is WhiteSpaceToken { 237 | return isQueryToken(token) && token.type === "WhiteSpace"; 238 | } 239 | 240 | export function isSeparatorToken(token: unknown): token is SeparatorToken { 241 | return isQueryToken(token) && token.type === "Separator"; 242 | } 243 | 244 | export function isMapToken(token: unknown): token is MapToken { 245 | return isQueryToken(token) && token.type === "Map"; 246 | } 247 | 248 | export function* query(value: string): Iterable { 249 | const lexer = new Chevrotain.Lexer(QueryParseTokens); 250 | const accessorLexer = new Chevrotain.Lexer(QueryAccessorParseTokens); 251 | 252 | const { tokens, errors } = lexer.tokenize(value.trim()); 253 | 254 | if (errors.length) { 255 | throw new Error(errors[0].message); 256 | } 257 | 258 | for (const token of tokens) { 259 | if (token.tokenType === Query.Selector) { 260 | const result: SelectorToken = { 261 | type: "Selector", 262 | text: token.image, 263 | image: token.image, 264 | }; 265 | yield result; 266 | } else if (token.tokenType === Query.Accessor) { 267 | const accessor = token.image.slice(1, -1); 268 | const { 269 | tokens: [left, operator, right, ...rest], 270 | } = accessorLexer.tokenize(accessor); 271 | 272 | // console.log({ left, operator, right, rest, accessor }); 273 | 274 | if (rest.length) { 275 | throw new Error(`Expected accessor with left, right, and operator`); 276 | } 277 | 278 | const result: AccessorToken = { 279 | type: "Accessor", 280 | text: token.image, 281 | accessor, 282 | left: getValue(left), 283 | operator: operator?.image, 284 | right: getValue(right), 285 | image: token.image, 286 | }; 287 | 288 | yield result; 289 | } else { 290 | const result = getValue(token); 291 | if (result) { 292 | yield result; 293 | } 294 | } 295 | } 296 | 297 | function getValue(token?: Chevrotain.IToken): QueryToken | undefined { 298 | if (!token) return undefined; 299 | if ( 300 | token.tokenType === Query.String || 301 | token.tokenType === QueryAccessor.String 302 | ) { 303 | return { 304 | type: "String", 305 | text: token.image.slice(1, -1), 306 | image: token.image, 307 | }; 308 | } 309 | if ( 310 | token.tokenType === Query.Raw || 311 | token.tokenType === QueryAccessor.Raw 312 | ) { 313 | return { 314 | type: "String", 315 | text: token.image.slice(3, -2), 316 | image: token.image, 317 | }; 318 | } 319 | if ( 320 | token.tokenType === Query.Number || 321 | token.tokenType === QueryAccessor.Number 322 | ) { 323 | return { 324 | type: "Number", 325 | number: +token.image, 326 | image: token.image, 327 | }; 328 | } 329 | if ( 330 | token.tokenType === Query.Boolean || 331 | token.tokenType === QueryAccessor.Boolean 332 | ) { 333 | return { 334 | type: "Boolean", 335 | boolean: token.image === "true", 336 | image: token.image, 337 | }; 338 | } 339 | if (token.tokenType === QueryAccessor.GetTag) { 340 | return { 341 | type: "GetTag", 342 | image: token.image, 343 | }; 344 | } 345 | if (token.tokenType === QueryAccessor.GetName) { 346 | return { 347 | type: "GetName", 348 | image: token.image, 349 | }; 350 | } 351 | if (token.tokenType === QueryAccessor.GetProperty) { 352 | const [, text] = token.image.slice(0, -1).split("("); 353 | return { 354 | type: "GetProperty", 355 | text, 356 | image: token.image, 357 | }; 358 | } 359 | if (token.tokenType === QueryAccessor.GetValue) { 360 | const [, index] = token.image.slice(0, -1).split("("); 361 | return { 362 | type: "GetValue", 363 | index: 364 | typeof index === "string" && /^\d+$/.test(index) ? +index : undefined, 365 | image: token.image, 366 | }; 367 | } 368 | if (token.tokenType === QueryAccessor.GetValues) { 369 | return { 370 | type: "GetValues", 371 | image: token.image, 372 | }; 373 | } 374 | if (token.tokenType === Query.Tag) { 375 | return { 376 | type: "Tag", 377 | text: token.image, 378 | tag: token.image.slice(1, -1), 379 | image: token.image, 380 | }; 381 | } 382 | return { 383 | type: token.tokenType.name, 384 | text: token.image, 385 | image: token.image, 386 | }; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/prepare.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccessorToken, 3 | isGetValuesToken, 4 | isAccessorToken, 5 | isBooleanToken, 6 | isGetNameToken, 7 | isGetPropertyToken, 8 | isGetTagToken, 9 | isGetValueToken, 10 | isNumberToken, 11 | isSelectorToken, 12 | isStringToken, 13 | query as parseString, 14 | QueryToken, 15 | isTopToken, 16 | isWhiteSpaceToken, 17 | isOrToken, 18 | isDirectChildToken, 19 | isImmediatelyFollowsToken, 20 | isFollowsToken, isSeparatorToken, isMapToken, MapToken, 21 | } from "./tokenizer"; 22 | import { 23 | children, 24 | getChildrenFromRawNode, 25 | isFragment, 26 | isFragmentResult, 27 | name, 28 | ok, 29 | properties, 30 | tag, 31 | values, 32 | } from "@virtualstate/focus"; 33 | import { split, Split } from "@virtualstate/promise"; 34 | import { isOperation, operator } from "./accessor"; 35 | import { isDefined, isIteratorYieldResult } from "./is"; 36 | 37 | export function prepare(node: unknown, queryInput: string) { 38 | const root = split({ 39 | async *[Symbol.asyncIterator]() { 40 | if (isFragment(node)) { 41 | yield* children(node); 42 | } else { 43 | yield node; 44 | } 45 | }, 46 | }); 47 | 48 | ok(root.mask); 49 | ok(root.filter); 50 | const tokens: QueryToken[] = [...parseString(queryInput)]; 51 | const queries = splitAt(tokens, isOrToken); 52 | 53 | let result: Split; 54 | for (let query of queries) { 55 | 56 | const lastIndexOfMap = query.findIndex(isMapToken); 57 | 58 | let map: MapToken; 59 | 60 | // console.log({ query, lastIndexOfMap }); 61 | 62 | if (lastIndexOfMap > -1) { 63 | const token = query.at(-1); 64 | if (isMapToken(token)) { 65 | query = query.slice(0, -1); 66 | map = token; 67 | } 68 | } 69 | 70 | 71 | let current = part(query).filter(isDefined); 72 | 73 | if (map) { 74 | 75 | let { image } = map; 76 | 77 | ok(image.startsWith("=>")); 78 | 79 | image = image.replace(/^=>\s+/, ""); 80 | 81 | let isTuple = false; 82 | 83 | if (image.startsWith("(")) { 84 | isTuple = true; 85 | ok(image.endsWith(")")); 86 | image = image.slice(1, -1); 87 | } 88 | 89 | const accessors = image.split(/\s*,\s*/g) 90 | .map(string => { 91 | const parsed = [...parseString(`[${string}]`)]; 92 | const [token] = parsed; 93 | ok(parsed.length === 1, "Expected single accessor token"); 94 | ok(isAccessorToken(token), "Expected accessor token"); 95 | return token; 96 | }); 97 | 98 | current = current.map(node => { 99 | const result = accessors.map(token => access(node, token)); 100 | if (isTuple) return result; 101 | return result.at(0); 102 | }) 103 | } 104 | 105 | if (result) { 106 | result = result.concat(current); 107 | } else { 108 | result = current; 109 | } 110 | } 111 | 112 | return split({ 113 | async *[Symbol.asyncIterator]() { 114 | ok(result); 115 | let last: unknown[] | undefined = undefined; 116 | for await (const snapshot of result) { 117 | if (isSameAsLast(snapshot)) { 118 | continue; 119 | } 120 | yield snapshot; 121 | last = snapshot; 122 | } 123 | 124 | function isSameAsLast(snapshot: unknown[]) { 125 | if (!last) return false; 126 | if (last.length !== snapshot.length) return false; 127 | return last.every((value, index) => snapshot[index] === value); 128 | } 129 | }, 130 | }); 131 | 132 | function startsWithSpace(query: QueryToken[]) { 133 | return isWhiteSpaceToken(query[0]); 134 | } 135 | 136 | function trimStart(query: QueryToken[]): QueryToken[] { 137 | if (!startsWithSpace(query)) return query; 138 | return trimStart(query.slice(1)); 139 | } 140 | 141 | function getNext(query: QueryToken[]) { 142 | const spaceIndex = query.findIndex(isWhiteSpaceToken); 143 | const isSpace = spaceIndex > -1; 144 | const tokens = isSpace ? query.slice(0, spaceIndex) : query; 145 | const rest = isSpace ? query.slice(spaceIndex + 1) : []; 146 | return [tokens, rest]; 147 | } 148 | 149 | function part(query: QueryToken[], result = root): Split { 150 | if (!query.length) return result; 151 | if (startsWithSpace(query)) { 152 | const rest = query.slice(1); 153 | const [follows] = rest; 154 | // Let direct child be handled as a primary token type 155 | if (follows && isDirectChildToken(follows)) { 156 | return part(rest, result); 157 | } else { 158 | return part(rest, children(result)); 159 | } 160 | } 161 | 162 | let [tokens, rest] = getNext(query); 163 | const [token] = tokens; 164 | 165 | ok(token); 166 | 167 | if (isDirectChildToken(token)) { 168 | ok(tokens.length === 1, "Please ensure there is a space after >"); 169 | let [next, afterNext] = getNext(rest); 170 | const [follows] = afterNext; 171 | let nextResult; 172 | if ( 173 | follows && 174 | (isImmediatelyFollowsToken(follows) || isFollowsToken(follows)) 175 | ) { 176 | afterNext = rest; 177 | nextResult = children(result).filter((_, __, nodes) => { 178 | const foundIndex = nodes.findIndex((node) => isMatch(node, next)); 179 | return foundIndex > -1; 180 | }); 181 | } else { 182 | nextResult = children(result).filter((node) => { 183 | return isMatch(node, next); 184 | }); 185 | } 186 | return part(afterNext, nextResult); 187 | } 188 | 189 | const [follows] = rest; 190 | 191 | let match: Split; 192 | let unmatch: Split; 193 | 194 | if (isTopToken(token)) { 195 | ok(tokens.length === 1, "Please ensure there is a space after top()"); 196 | const masked = root.mask(mask(result)); 197 | if (!follows) { 198 | return masked; 199 | } 200 | if (isDirectChildToken(follows)) { 201 | return part(rest, masked); 202 | } 203 | return part(rest, children(masked)); 204 | } else if ( 205 | follows && 206 | (isImmediatelyFollowsToken(follows) || isFollowsToken(follows)) 207 | ) { 208 | const [next, afterNext] = getNext(trimStart(rest.slice(1))); 209 | const left = tokens; 210 | const right = next; 211 | 212 | function isFollowsMatch( 213 | current: unknown, 214 | index: number, 215 | array: unknown[] 216 | ) { 217 | const before = array.slice(0, index); 218 | if (!before.length) return false; 219 | if (!isMatch(current, right)) return false; 220 | if (isImmediatelyFollowsToken(follows)) { 221 | return isMatch(before.at(-1), left); 222 | } 223 | 224 | const foundIndex = before.findIndex((node) => isMatch(node, left)); 225 | return foundIndex > -1; 226 | } 227 | 228 | rest = afterNext; 229 | match = result.filter(isFollowsMatch); 230 | unmatch = result.filter((...args) => !isFollowsMatch(...args)); 231 | } else { 232 | match = result.filter((node) => isMatch(node, tokens)); 233 | 234 | if (tokens.length === 1 && isAccessorToken(token) && !token.accessor) { 235 | unmatch = match; 236 | } else { 237 | unmatch = result.filter((node) => !isMatch(node, tokens)); 238 | } 239 | } 240 | 241 | ok(match); 242 | ok(unmatch); 243 | 244 | const unmatchResult = unmatch.flatMap((node) => 245 | part(query, children(node)) 246 | ); 247 | if (!rest.length) { 248 | return match.concat(unmatchResult); 249 | } 250 | return part(rest, match).concat(unmatchResult); 251 | } 252 | } 253 | 254 | function isMatch(node: unknown, tokens: QueryToken[]) { 255 | for (const token of tokens) { 256 | const result = access(node, token); 257 | if (Array.isArray(result)) { 258 | if (!result.length) { 259 | return false; 260 | } 261 | } else { 262 | if (!result) { 263 | return false; 264 | } 265 | } 266 | } 267 | return true; 268 | } 269 | 270 | function access(node: unknown, token: QueryToken): unknown { 271 | if (isAccessorToken(token)) { 272 | return getFromAccessor(node, token); 273 | } 274 | if (isSelectorToken(token)) { 275 | return name(node) === token.text; 276 | } 277 | if (isBooleanToken(token)) { 278 | return token.boolean; 279 | } 280 | if (isStringToken(token)) { 281 | return token.text; 282 | } 283 | if (isNumberToken(token)) { 284 | return token.number; 285 | } 286 | if (isGetPropertyToken(token)) { 287 | return properties(node)[token.text]; 288 | } 289 | if (isGetNameToken(token)) { 290 | return name(node); 291 | } 292 | if (isGetValueToken(token)) { 293 | const maybe = getValues(); 294 | if (!Array.isArray(maybe)) return undefined; 295 | return maybe[token.index ?? 0]; 296 | } 297 | if (isGetValuesToken(token)) { 298 | return getValues(); 299 | } 300 | if (isGetTagToken(token)) { 301 | return tag(node); 302 | } 303 | 304 | console.error({ token }); 305 | throw new Error(`Unhandled accessor ${token}`); 306 | 307 | function getValues() { 308 | const iterable = values(node); 309 | const maybe = getChildrenFromRawNode(node); 310 | if (Array.isArray(iterable) && !iterable.length) { 311 | if (Array.isArray(maybe) && maybe.length) { 312 | return maybe; 313 | } 314 | } 315 | return iterable; 316 | } 317 | 318 | // return undefined 319 | } 320 | 321 | function getFromAccessor(node: unknown, token: AccessorToken): unknown { 322 | const { accessor, left, right, operator: op } = token; 323 | 324 | if (left && right && op) { 325 | ok(isOperation(op)); 326 | return operator(access(node, left), access(node, right), op); 327 | } 328 | 329 | if (left) { 330 | return access(node, left); 331 | } 332 | 333 | return typeof accessor === "string" && accessor.length === 0; 334 | } 335 | 336 | function splitAt( 337 | array: T[], 338 | fn: (value: T) => boolean 339 | ): T[][] { 340 | const result: T[][] = []; 341 | let working: T[] = []; 342 | for (const value of array) { 343 | if (fn(value)) { 344 | if (working.length) { 345 | if (isWhiteSpaceToken(working.at(-1))) { 346 | working.splice(-1, 1); 347 | } 348 | result.push(working); 349 | working = []; 350 | } 351 | continue; 352 | } 353 | if (!working.length && isWhiteSpaceToken(value)) { 354 | continue; 355 | } 356 | working.push(value); 357 | } 358 | if (working.length) { 359 | result.push(working); 360 | } 361 | return result; 362 | } 363 | 364 | function mask(result: Split) { 365 | return { 366 | async *[Symbol.asyncIterator]() { 367 | const iterator = result[Symbol.asyncIterator](); 368 | let iteratorResult = await iterator.next(); 369 | 370 | if (!isIteratorYieldResult(iteratorResult)) return; 371 | 372 | let lastIteratorResult = iteratorResult; 373 | 374 | yield isVisible(); 375 | 376 | /* 377 | After our first yield, we're going to just blindly return 378 | the last result we had found, so if we only ever found 379 | one version of our original, we can keep on providing the 380 | mask 381 | 382 | split is designed to grab one mask iteration for each possible 383 | yielding iteration, meaning the mask can block until it has a result 384 | which we did on the yield before 385 | */ 386 | 387 | let error: unknown = undefined; 388 | 389 | let promise: Promise | undefined = undefined; 390 | 391 | try { 392 | while (!iteratorResult.done) { 393 | next(); 394 | yield isVisible(); 395 | } 396 | } finally { 397 | await iterator.return?.(); 398 | } 399 | 400 | function next() { 401 | if (error) { 402 | throw error; 403 | } 404 | if (!promise) { 405 | promise = iterator 406 | .next() 407 | .finally(() => { 408 | promise = undefined; 409 | }) 410 | .then((value) => { 411 | iteratorResult = value; 412 | if (isIteratorYieldResult(iteratorResult)) { 413 | lastIteratorResult = iteratorResult; 414 | } 415 | }) 416 | .catch((reason) => { 417 | error = reason; 418 | }); 419 | } 420 | } 421 | 422 | function isVisible() { 423 | return lastIteratorResult.value.length > 0; 424 | } 425 | }, 426 | }; 427 | } 428 | --------------------------------------------------------------------------------