@p-buddy/parkdown 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +14 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +396 -0
- package/dist/index.umd.cjs +15 -0
- package/package.json +9 -1
- package/.assets/api-note.md +0 -3
- package/.assets/api.md +0 -34
- package/.assets/authoring.md +0 -69
- package/.assets/code/depopulate.ts +0 -6
- package/.assets/code/inclusions.ts +0 -6
- package/.assets/depopulated.md +0 -25
- package/.assets/invocation.md +0 -16
- package/.assets/populated/block.md +0 -9
- package/.assets/populated/inline.multi.md +0 -5
- package/.assets/populated/inline.single.md +0 -3
- package/.assets/query.md +0 -73
- package/.assets/remap-imports.md +0 -0
- package/.assets/unpopulated/block.md +0 -5
- package/.assets/unpopulated/inline.multi.md +0 -3
- package/.assets/unpopulated/inline.single.md +0 -1
- package/.devcontainer/Dockerfile +0 -16
- package/.devcontainer/devcontainer.json +0 -35
- package/src/api/index.test.ts +0 -32
- package/src/api/index.ts +0 -8
- package/src/api/types.ts +0 -78
- package/src/api/utils.test.ts +0 -132
- package/src/api/utils.ts +0 -161
- package/src/cli.ts +0 -31
- package/src/include.test.ts +0 -369
- package/src/include.ts +0 -252
- package/src/index.ts +0 -35
- package/src/region.test.ts +0 -145
- package/src/region.ts +0 -138
- package/src/remap.test.ts +0 -37
- package/src/remap.ts +0 -72
- package/src/utils.test.ts +0 -238
- package/src/utils.ts +0 -184
- package/src/wrap.ts +0 -61
- package/tsconfig.json +0 -5
- package/vite.cli.config.ts +0 -23
- package/vite.config.ts +0 -20
package/.assets/query.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Query parameters
|
|
2
|
-
|
|
3
|
-
You can pass query parameters to your inclusion links to control how their content is processed and included within your markdown.
|
|
4
|
-
|
|
5
|
-
## Processing Order
|
|
6
|
-
|
|
7
|
-
[](../src/include.ts?®ion=extract(query))
|
|
8
|
-
|
|
9
|
-
## `region`
|
|
10
|
-
|
|
11
|
-
Either extract, remove, or replace content from the included file based on the provided specifier(s).
|
|
12
|
-
|
|
13
|
-
Specifiers will be searched for within the file's comments, and are expected to come in pairs / bookend the desired region, like so:
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
/** some-specifier */
|
|
17
|
-
... code to find ...
|
|
18
|
-
/** some-specifier */
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
```md
|
|
22
|
-
[](...?region=extract(some-specifier))
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Below is the currently supported API for the `region` query parameter, where each defined method signature can be _invoked_ as a value for the `region` parameter (e.g. `[](<url>?region=extract(some-specifier))`, `[](<url>?region=remove(some-specifier))`, `[](<url>?region=replace(some-specifier))`).
|
|
26
|
-
|
|
27
|
-
[](./api-note.md?wrap=quote)
|
|
28
|
-
|
|
29
|
-
[](../src/region.ts?region=extract(definition))
|
|
30
|
-
|
|
31
|
-
## `skip`
|
|
32
|
-
|
|
33
|
-
Skip the default processing behavior for the given type of file.
|
|
34
|
-
|
|
35
|
-
[](../src/include.ts?wrap=dropdown(See-default-processing-behavior.)®ion=extract(Default-Behavior),replace(...))
|
|
36
|
-
|
|
37
|
-
```md
|
|
38
|
-
[](<url>?skip)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## `heading`
|
|
42
|
-
|
|
43
|
-
Modify the heading depth applied to included content. By default, the headings of the included content are adjusted to be one-level below their parent heading.
|
|
44
|
-
|
|
45
|
-
In the following example, the headings within the included content of `<url>` will be adjusted to one-level below the parent heading (which is an `h2` / `##`), so any `#` headings will be converted to `###` headings, and `##` headings will be converted to `####` headings, and so on.
|
|
46
|
-
|
|
47
|
-
```md
|
|
48
|
-
## Heading
|
|
49
|
-
|
|
50
|
-
[](<url>)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
The following would then ensure that the headings of the included content are at the same level as the parent heading.
|
|
54
|
-
|
|
55
|
-
```md
|
|
56
|
-
## Heading
|
|
57
|
-
|
|
58
|
-
[](<url>?heading=-1)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
A value of `-2` would result in the headings of the included content being at their original level (since the content is being included underneath an `h2` / `##` heading).
|
|
62
|
-
|
|
63
|
-
## `inline` (Advanced)
|
|
64
|
-
|
|
65
|
-
## `wrap`
|
|
66
|
-
|
|
67
|
-
Wrap the content of the included file in a specific kind of element.
|
|
68
|
-
|
|
69
|
-
Below is the currently supported API for the `wrap` query parameter, where each defined method signature can be _invoked_ as a value for the `wrap` parameter (e.g. `[](<url>?wrap=code)`, `[](<url>?wrap=dropdown(hello-world))`).
|
|
70
|
-
|
|
71
|
-
[](./api-note.md?wrap=quote)
|
|
72
|
-
|
|
73
|
-
[](../src/wrap.ts?region=extract(definition))
|
package/.assets/remap-imports.md
DELETED
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Before... [](<url>) ...After
|
package/.devcontainer/Dockerfile
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/typescript-node/.devcontainer/base.Dockerfile
|
|
2
|
-
|
|
3
|
-
# [Choice] Node.js version: 16, 18, 20
|
|
4
|
-
ARG VARIANT="20-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 <your-package-list-here>
|
|
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
|
-
# To install more global node packages
|
|
16
|
-
RUN su node -c "npm install -g pnpm@10"
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "parkdown-dev",
|
|
3
|
-
"dockerFile": "Dockerfile",
|
|
4
|
-
"customizations": {
|
|
5
|
-
"vscode": {
|
|
6
|
-
"extensions": [
|
|
7
|
-
"ms-vscode.vscode-typescript-next",
|
|
8
|
-
"svelte.svelte-vscode",
|
|
9
|
-
"ms-azuretools.vscode-docker",
|
|
10
|
-
"bradlc.vscode-tailwindcss",
|
|
11
|
-
"YoavBls.pretty-ts-errors"
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"postStartCommand": [
|
|
16
|
-
"bash",
|
|
17
|
-
"-c",
|
|
18
|
-
"git config --global user.name \"${GIT_USER_NAME}\"",
|
|
19
|
-
"bash",
|
|
20
|
-
"-c",
|
|
21
|
-
"git config --global user.email \"${GIT_USER_EMAIL}\"",
|
|
22
|
-
"bash",
|
|
23
|
-
"-c",
|
|
24
|
-
"pnpm install"
|
|
25
|
-
],
|
|
26
|
-
"mounts": [
|
|
27
|
-
"source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,readonly"
|
|
28
|
-
],
|
|
29
|
-
"containerEnv": {
|
|
30
|
-
"NPM_TOKEN": "${localEnv:PARKDOWN_NPM_TOKEN}",
|
|
31
|
-
"HOST_WORKSPACE_PATH": "${localWorkspaceFolder}",
|
|
32
|
-
"GIT_USER_NAME": "${localEnv:GIT_USER_NAME}",
|
|
33
|
-
"GIT_USER_EMAIL": "${localEnv:GIT_USER_EMAIL}"
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/api/index.test.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { MethodDefinition } from "./types";
|
|
3
|
-
import { numberedParameters, createParser } from "./index";
|
|
4
|
-
|
|
5
|
-
describe(numberedParameters.name, () => {
|
|
6
|
-
test("simple", () => {
|
|
7
|
-
const definitions = [
|
|
8
|
-
"example(0: string, 1?: string)" satisfies MethodDefinition,
|
|
9
|
-
] as const;
|
|
10
|
-
|
|
11
|
-
const result = createParser(definitions)("example(z,a)");
|
|
12
|
-
expect(numberedParameters(result)).toEqual(["z", "a"]);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("mixed types", () => {
|
|
16
|
-
const definitions = [
|
|
17
|
-
"example(0: string, 1?: string, 2: boolean)" satisfies MethodDefinition,
|
|
18
|
-
] as const;
|
|
19
|
-
|
|
20
|
-
const result = createParser(definitions)("example(z,a,true)");
|
|
21
|
-
expect(numberedParameters(result)).toEqual(["z", "a", true]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("mixed positions", () => {
|
|
25
|
-
const definitions = [
|
|
26
|
-
"example(0: string, hi: string, 1: boolean)" satisfies MethodDefinition,
|
|
27
|
-
] as const;
|
|
28
|
-
|
|
29
|
-
const result = createParser(definitions)("example(z,hi,true)");
|
|
30
|
-
expect(numberedParameters(result)).toEqual(["z", true]);
|
|
31
|
-
});
|
|
32
|
-
});
|
package/src/api/index.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export type { MethodDefinition } from "./types";
|
|
2
|
-
import type { Parser } from "./types";
|
|
3
|
-
export { createParser } from "./utils";
|
|
4
|
-
|
|
5
|
-
export const numberedParameters = (result: ReturnType<Parser<any>>) =>
|
|
6
|
-
Object.entries(result)
|
|
7
|
-
.filter(([key]) => key !== "name" && !isNaN(Number(key)))
|
|
8
|
-
.map(([_, value]) => value);
|
package/src/api/types.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
export type TypeRecord = {
|
|
2
|
-
"string": string,
|
|
3
|
-
"number": number,
|
|
4
|
-
"boolean": boolean,
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
type ExpandRecursively<T> =
|
|
8
|
-
T extends (...args: infer A) => infer R
|
|
9
|
-
/**/ ? (...args: ExpandRecursively<A>) => ExpandRecursively<R>
|
|
10
|
-
/**/ : T extends object
|
|
11
|
-
/**/ ? T extends infer O
|
|
12
|
-
/**/ ? { [K in keyof O]: ExpandRecursively<O[K]> }
|
|
13
|
-
/**/ : never
|
|
14
|
-
/**/ : T;
|
|
15
|
-
|
|
16
|
-
type WithoutLeadingWhitespace<T extends string> = T extends ` ${infer Without}` ? Without : T;
|
|
17
|
-
|
|
18
|
-
type SplitOn<T extends string, Delimeter extends string> =
|
|
19
|
-
T extends `${infer Left}${Delimeter}${infer Right}` ? [Left, ...SplitOn<Right, Delimeter>] : [T];
|
|
20
|
-
|
|
21
|
-
type RepeatInTuple<Element, Count extends number, Acc extends unknown[] = []> = {
|
|
22
|
-
[k in Count]: Acc['length'] extends k
|
|
23
|
-
? Acc
|
|
24
|
-
: RepeatInTuple<Element, k, [...Acc, Element]>;
|
|
25
|
-
}[Count];
|
|
26
|
-
|
|
27
|
-
type Join<T extends string[], D extends string> =
|
|
28
|
-
T extends []
|
|
29
|
-
/**/ ? ''
|
|
30
|
-
/**/ : T extends [infer F extends string]
|
|
31
|
-
/**/ ? F
|
|
32
|
-
/**/ : T extends [infer F extends string, ...infer R extends string[]]
|
|
33
|
-
/**/ ? `${F}${D}${Join<R, D>}`
|
|
34
|
-
/**/ : string;
|
|
35
|
-
|
|
36
|
-
type Capture<T extends string, Start extends string, End extends string> = {
|
|
37
|
-
[k in T]: k extends `${infer Before}${Start}${infer Captured}${End}${infer After}`
|
|
38
|
-
/**/ ? { before: Before, captured: Captured, after: After }
|
|
39
|
-
/**/ : { before: T, captured: "", after: "" };
|
|
40
|
-
}[T]
|
|
41
|
-
|
|
42
|
-
type SplitOnComma<T extends string> = { [k in SplitOn<T, ",">[number]]: k }[SplitOn<T, ",">[number]];
|
|
43
|
-
|
|
44
|
-
type Parameter<Name extends string, Optional extends boolean, Type extends TypeRecord[keyof TypeRecord]> = { name: Name, optional: Optional, type: Type };
|
|
45
|
-
|
|
46
|
-
type ExtractParameter<T extends string> = T extends `${infer Name}?: ${infer Type extends keyof TypeRecord}`
|
|
47
|
-
/**/ ? Parameter<Name, true, Type>
|
|
48
|
-
/**/ : T extends `${infer Name}: ${infer Type extends keyof TypeRecord}`
|
|
49
|
-
/**/ ? Parameter<Name, false, Type>
|
|
50
|
-
/**/ : never;
|
|
51
|
-
|
|
52
|
-
type FunctionName<T extends string> = Capture<T, "(", ")">["before"];
|
|
53
|
-
|
|
54
|
-
type ParameterRecord<T extends string> = {
|
|
55
|
-
[k in WithoutLeadingWhitespace<SplitOnComma<Capture<T, "(", ")">["captured"]>> as ExtractParameter<k>["name"]]: ExtractParameter<k>["optional"] extends true ? TypeRecord[ExtractParameter<k>["type"]] | undefined : TypeRecord[ExtractParameter<k>["type"]]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type UndefinedToOptional<T> = {
|
|
59
|
-
[K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>;
|
|
60
|
-
} & {
|
|
61
|
-
[K in keyof T as undefined extends T[K] ? never : K]: T[K];
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
type ParseMethodDefinition<T extends string> = { [t in T as FunctionName<t>]: UndefinedToOptional<ParameterRecord<t>> };
|
|
65
|
-
|
|
66
|
-
type ParameterDefinition = `${string}${"?" | ""}: ${keyof TypeRecord}`;
|
|
67
|
-
/** Can add more parameters by adding more numbers to the count union, but it adds a lot of complexity to the type */
|
|
68
|
-
type ZeroOrMoreParameters = Join<RepeatInTuple<ParameterDefinition, 0 | 1 | 2>, ", ">;
|
|
69
|
-
export type MethodDefinition = `${string}(${ZeroOrMoreParameters})`;
|
|
70
|
-
|
|
71
|
-
export type MethodDefinitions = string[] | readonly string[];
|
|
72
|
-
|
|
73
|
-
export type ParsingCandidates<T extends MethodDefinitions> = ExpandRecursively<{
|
|
74
|
-
[k in keyof ParseMethodDefinition<T[number]>]: ParseMethodDefinition<T[number]>[k] & { name: k }
|
|
75
|
-
}[keyof ParseMethodDefinition<T[number]>]>;
|
|
76
|
-
|
|
77
|
-
export type Parser<T extends MethodDefinitions> = (query: string) => ParsingCandidates<T>;
|
|
78
|
-
export type CreateParser = <T extends MethodDefinitions>(definitions: T) => Parser<T>;
|
package/src/api/utils.test.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { parseInvocation, parseDefinition, createParser, splitOnUnquotedComma } from "./utils";
|
|
3
|
-
import { MethodDefinition } from "./types";
|
|
4
|
-
import { COMMA_NOT_IN_PARENTHESIS } from "../utils";
|
|
5
|
-
|
|
6
|
-
describe(parseInvocation.name, () => {
|
|
7
|
-
const testCases = [
|
|
8
|
-
["code(ts,some-meta)", { name: "code", parameters: ["ts", "some-meta"] }],
|
|
9
|
-
["code", { name: "code", parameters: [] }],
|
|
10
|
-
["code()", { name: "code", parameters: [] }],
|
|
11
|
-
["code(ts)", { name: "code", parameters: ["ts"] }],
|
|
12
|
-
["code(,some-meta)", { name: "code", parameters: [undefined, "some-meta"] }],
|
|
13
|
-
["code(,some-meta,,,)", { name: "code", parameters: [undefined, "some-meta"] }],
|
|
14
|
-
["dropdown(hello-world,true)", { name: "dropdown", parameters: ["hello-world", "true"] }],
|
|
15
|
-
["code(,,anything)", { name: "code", parameters: [undefined, undefined, "anything"] }],
|
|
16
|
-
] as const;
|
|
17
|
-
|
|
18
|
-
for (const [input, expected] of testCases) {
|
|
19
|
-
test(input, () => {
|
|
20
|
-
expect(parseInvocation(input)).toEqual(expected);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe(parseDefinition.name, () => {
|
|
26
|
-
const testCases = [
|
|
27
|
-
["code(lang?: string, meta?: string)", {
|
|
28
|
-
name: "code",
|
|
29
|
-
parameters: [
|
|
30
|
-
{ name: "lang", optional: true, type: "string" },
|
|
31
|
-
{ name: "meta", optional: true, type: "string" }
|
|
32
|
-
]
|
|
33
|
-
}],
|
|
34
|
-
["quote", { name: "quote" }],
|
|
35
|
-
["dropdown(summary: string, open?: boolean)", {
|
|
36
|
-
name: "dropdown",
|
|
37
|
-
parameters: [
|
|
38
|
-
{ name: "summary", optional: false, type: "string" },
|
|
39
|
-
{ name: "open", optional: true, type: "boolean" }
|
|
40
|
-
]
|
|
41
|
-
}],
|
|
42
|
-
["allTypes(a: string, b: boolean, c: number, d?: string, e?: boolean, f?: number)", {
|
|
43
|
-
name: "allTypes",
|
|
44
|
-
parameters: [
|
|
45
|
-
{ name: "a", optional: false, type: "string" },
|
|
46
|
-
{ name: "b", optional: false, type: "boolean" },
|
|
47
|
-
{ name: "c", optional: false, type: "number" },
|
|
48
|
-
{ name: "d", optional: true, type: "string" },
|
|
49
|
-
{ name: "e", optional: true, type: "boolean" },
|
|
50
|
-
{ name: "f", optional: true, type: "number" },
|
|
51
|
-
]
|
|
52
|
-
}]
|
|
53
|
-
] as const;
|
|
54
|
-
|
|
55
|
-
for (const [input, expected] of testCases) {
|
|
56
|
-
test(input, () => {
|
|
57
|
-
expect(parseDefinition(input)).toEqual(expected);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe(createParser.name, () => {
|
|
63
|
-
const definitions = [
|
|
64
|
-
"code(lang?: string, meta?: string)" satisfies MethodDefinition,
|
|
65
|
-
"quote",
|
|
66
|
-
"dropdown(summary: string, open?: boolean)" satisfies MethodDefinition,
|
|
67
|
-
] as const;
|
|
68
|
-
|
|
69
|
-
const parse = createParser(definitions);
|
|
70
|
-
|
|
71
|
-
const invocations = [
|
|
72
|
-
["code", { name: "code" }],
|
|
73
|
-
["code()", { name: "code" }],
|
|
74
|
-
["code(ts)", { name: "code", lang: "ts" }],
|
|
75
|
-
["code(ts,some-meta)", { name: "code", lang: "ts", meta: "some-meta" }],
|
|
76
|
-
["code(,some-meta)", { name: "code", meta: "some-meta" }],
|
|
77
|
-
["code(,some-meta,)", { name: "code", meta: "some-meta" }],
|
|
78
|
-
["code(,,anything)", { error: true }],
|
|
79
|
-
["quote", { name: "quote" }],
|
|
80
|
-
["quote()", { name: "quote" }],
|
|
81
|
-
["dropdown()", { error: true }],
|
|
82
|
-
["dropdown(,true)", { error: true }],
|
|
83
|
-
["dropdown(hello-world,true)", { name: "dropdown", summary: "hello-world", open: true }],
|
|
84
|
-
["dropdown('hello world',true)", { name: "dropdown", summary: "hello world", open: true }],
|
|
85
|
-
|
|
86
|
-
] as const;
|
|
87
|
-
|
|
88
|
-
for (const [input, expected] of invocations) {
|
|
89
|
-
test(input, () => {
|
|
90
|
-
if ('error' in expected) {
|
|
91
|
-
expect(() => parse(input)).toThrow();
|
|
92
|
-
} else {
|
|
93
|
-
expect(parse(input)).toEqual(expected);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
describe("split on non-parenthesized comma", () => {
|
|
102
|
-
const testCases = [
|
|
103
|
-
["hello,world", ["hello", "world"]],
|
|
104
|
-
["code(,,anything), code, quote, dropdown(hello-world,true)", ["code(,,anything)", "code", "quote", "dropdown(hello-world,true)"]],
|
|
105
|
-
] as const;
|
|
106
|
-
|
|
107
|
-
for (const [input, expected] of testCases) {
|
|
108
|
-
test(input, () => {
|
|
109
|
-
expect(input.split(COMMA_NOT_IN_PARENTHESIS)).toEqual(expected);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
describe(splitOnUnquotedComma.name, () => {
|
|
117
|
-
test("simple", () => {
|
|
118
|
-
expect(splitOnUnquotedComma("hello,world")).toEqual(["hello", "world"]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("single quotes", () => {
|
|
122
|
-
expect(splitOnUnquotedComma("hello,'hello,world',hello")).toEqual(["hello", "hello,world", "hello"]);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("preserve dual single quotes", () => {
|
|
126
|
-
expect(splitOnUnquotedComma("hello,''hello,world'',hello")).toEqual(["hello", "''hello,world''", "hello"]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("preserve triple double quotes", () => {
|
|
130
|
-
expect(splitOnUnquotedComma("hello,'''hello,world''',hello")).toEqual(["hello", "'''hello,world'''", "hello"]);
|
|
131
|
-
});
|
|
132
|
-
})
|
package/src/api/utils.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import type { CreateParser, MethodDefinitions, ParsingCandidates, TypeRecord } from "./types";
|
|
2
|
-
|
|
3
|
-
const supportedTypes = ["string", "number", "boolean"] as const satisfies (keyof TypeRecord)[];
|
|
4
|
-
|
|
5
|
-
const isSupportedType = (type: string): type is keyof TypeRecord => supportedTypes.includes(type as keyof TypeRecord);
|
|
6
|
-
|
|
7
|
-
// This regex is used to parse function-like invocations such as "code(ts,some-meta)"
|
|
8
|
-
// Breaking down the regex pattern:
|
|
9
|
-
// ^ - Asserts position at the start of the string
|
|
10
|
-
// ([a-zA-Z0-9_-]+) - Capture group 1: The function name
|
|
11
|
-
// [a-zA-Z0-9_-]+ - One or more alphanumeric characters, underscores, or hyphens
|
|
12
|
-
// (?:\(([^)]*)\))? - Optional non-capturing group for the parameters section
|
|
13
|
-
// \( - Literal opening parenthesis
|
|
14
|
-
// ([^)]*) - Capture group 2: Zero or more characters that are not closing parentheses
|
|
15
|
-
// \) - Literal closing parenthesis
|
|
16
|
-
// ? - Makes the entire parameter section optional
|
|
17
|
-
// $ - Asserts position at the end of the string
|
|
18
|
-
const INVOCATION_REGEX = /^([a-zA-Z0-9_-]+)(?:\(([^)]*)\))?$/;
|
|
19
|
-
|
|
20
|
-
export const parseInvocation = (input: string): { name: string, parameters: (string | undefined)[] } => {
|
|
21
|
-
const match = input.match(INVOCATION_REGEX);
|
|
22
|
-
if (!match) throw new Error(`Invalid invocation: ${input}`);
|
|
23
|
-
|
|
24
|
-
const [, name, rawParams = ""] = match;
|
|
25
|
-
|
|
26
|
-
if (!rawParams.trim()) return { name, parameters: [] };
|
|
27
|
-
|
|
28
|
-
const parameters = splitOnUnquotedComma(rawParams);
|
|
29
|
-
return { name, parameters };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const restIsUndefined = <T>(arr: T[], index: number) => {
|
|
33
|
-
for (let i = index + 1; i < arr.length; i++)
|
|
34
|
-
if (arr[i] !== undefined) return false;
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const splitOnUnquotedComma = (input: string): (string | undefined)[] => {
|
|
39
|
-
const result: (string | undefined)[] = [];
|
|
40
|
-
|
|
41
|
-
let current = "";
|
|
42
|
-
let inSingleQuotes = false;
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < input.length; i++) {
|
|
45
|
-
const ch = input[i];
|
|
46
|
-
const isSingleQuote = ch === "'";
|
|
47
|
-
|
|
48
|
-
if (isSingleQuote && input.at(i + 1) === "'") {
|
|
49
|
-
const triple = input.at(i + 2) === "'";
|
|
50
|
-
inSingleQuotes = !inSingleQuotes;
|
|
51
|
-
current += triple ? "'''" : "''";
|
|
52
|
-
i += triple ? 2 : 1;
|
|
53
|
-
} else if (isSingleQuote) {
|
|
54
|
-
inSingleQuotes = !inSingleQuotes;
|
|
55
|
-
current += ch;
|
|
56
|
-
} else if (ch === "," && !inSingleQuotes) {
|
|
57
|
-
result.push(current.trim());
|
|
58
|
-
current = "";
|
|
59
|
-
} else current += ch;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
result.push(current.trim());
|
|
63
|
-
|
|
64
|
-
return result
|
|
65
|
-
.map(item => item === "" ? undefined : item ? stripSurroundingSingleQuotes(item) : undefined)
|
|
66
|
-
.filter((item, index, arr) => item !== undefined || !restIsUndefined(arr, index));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const stripSurroundingSingleQuotes = (input: string): string => {
|
|
70
|
-
const isWrapped = input.length >= 2 && input[0] === "'" && input.at(-1) === "'";
|
|
71
|
-
const isDoubleWrapped = isWrapped && (input.length >= 4 && input[1] === "'" && input.at(-2) === "'");
|
|
72
|
-
return !isWrapped || isDoubleWrapped ? input : input.slice(1, -1);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
type ParsedParameterDefinition = { name: string; optional: boolean; type: keyof TypeRecord };
|
|
76
|
-
type ParsedMethodDefinition = { name: string; parameters?: ParsedParameterDefinition[] };
|
|
77
|
-
|
|
78
|
-
export const parseDefinition = <T extends string>(definition: T): ParsedMethodDefinition => {
|
|
79
|
-
// Match a method definition pattern:
|
|
80
|
-
// ^ - start of string
|
|
81
|
-
// (\w+) - capture group 1: one or more word characters (method name)
|
|
82
|
-
// (?:\(([^)]*)\))? - optional non-capturing group:
|
|
83
|
-
// \( - literal opening parenthesis
|
|
84
|
-
// ([^)]*) - capture group 2: zero or more characters that are not closing parenthesis (parameter list)
|
|
85
|
-
// \) - literal closing parenthesis
|
|
86
|
-
const METHOD_REGEX = /^(\w+)(?:\(([^)]*)\))?/;
|
|
87
|
-
|
|
88
|
-
const methodMatch = definition.match(METHOD_REGEX);
|
|
89
|
-
if (!methodMatch) return { name: definition };
|
|
90
|
-
|
|
91
|
-
const [, methodName, paramString] = methodMatch;
|
|
92
|
-
|
|
93
|
-
if (!paramString) return { name: methodName };
|
|
94
|
-
|
|
95
|
-
// (\w+) - Capture group 1: One or more word characters (parameter name)
|
|
96
|
-
// (\?)? - Capture group 2: Optional question mark (indicates optional parameter)
|
|
97
|
-
// : - Literal colon character
|
|
98
|
-
// \s* - Zero or more whitespace characters
|
|
99
|
-
// ([^,]+) - Capture group 3: One or more characters that are not commas (parameter type)
|
|
100
|
-
// /g - Global flag: Find all matches in the string, not just the first one
|
|
101
|
-
const PARAM_REGEX = /(\w+)(\?)?:\s*([^,]+)/g;
|
|
102
|
-
|
|
103
|
-
const parameters: ParsedParameterDefinition[] = [];
|
|
104
|
-
|
|
105
|
-
let match: RegExpExecArray | null;
|
|
106
|
-
while ((match = PARAM_REGEX.exec(paramString)) !== null) {
|
|
107
|
-
const [, name, optSymbol, paramType] = match;
|
|
108
|
-
const type = paramType.trim();
|
|
109
|
-
if (!isSupportedType(type)) throw new Error(`Unsupported type: ${type}`);
|
|
110
|
-
parameters.push({ name, optional: optSymbol === "?", type });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return { name: methodName, parameters };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export const createParser: CreateParser = <T extends MethodDefinitions>(definitions: T) => {
|
|
117
|
-
const parsedDefinitions = definitions
|
|
118
|
-
.map(parseDefinition)
|
|
119
|
-
.reduce(
|
|
120
|
-
(map, { name, parameters }) => map.set(name, parameters),
|
|
121
|
-
new Map<string, ParsedParameterDefinition[] | undefined>()
|
|
122
|
-
);
|
|
123
|
-
return (query: string) => {
|
|
124
|
-
const { name, parameters } = parseInvocation(query);
|
|
125
|
-
if (!parsedDefinitions.has(name))
|
|
126
|
-
throw new Error(`Unknown method: ${name}`);
|
|
127
|
-
|
|
128
|
-
const parameterDefinitions = parsedDefinitions.get(name);
|
|
129
|
-
if (parameterDefinitions === undefined)
|
|
130
|
-
return { name } satisfies ParsingCandidates<[string]> as ParsingCandidates<T>;
|
|
131
|
-
|
|
132
|
-
if (parameters.length > parameterDefinitions.length) {
|
|
133
|
-
const requiredCount = parameterDefinitions.filter(({ optional }) => !optional).length;
|
|
134
|
-
const expected = requiredCount === parameterDefinitions.length
|
|
135
|
-
? requiredCount.toString()
|
|
136
|
-
: `${requiredCount} - ${parameterDefinitions.length}`;
|
|
137
|
-
throw new Error(`Too many parameters: ${parameters.length} for method '${name}' (expected: ${expected})`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return parameterDefinitions.reduce((acc, { name: param, optional, type }, index) => {
|
|
141
|
-
const value = parameters[index];
|
|
142
|
-
|
|
143
|
-
if (value === undefined)
|
|
144
|
-
if (optional) return acc;
|
|
145
|
-
else throw new Error(`Missing required parameter: ${param} for method '${name}'`);
|
|
146
|
-
|
|
147
|
-
switch (type) {
|
|
148
|
-
case "string":
|
|
149
|
-
acc[param] = value;
|
|
150
|
-
break;
|
|
151
|
-
case "number":
|
|
152
|
-
case "boolean":
|
|
153
|
-
acc[param] = JSON.parse(value);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
if (typeof acc[param] !== type)
|
|
157
|
-
throw new Error(`Invalid type: ${param} must be ${type}, got ${typeof acc[param]} for method '${name}'`);
|
|
158
|
-
return acc;
|
|
159
|
-
}, { name } as ParsingCandidates<T>);
|
|
160
|
-
}
|
|
161
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from '@commander-js/extra-typings';
|
|
3
|
-
import { version } from '../package.json';
|
|
4
|
-
import { populateMarkdownInclusions, depopulateMarkdownInclusions } from '.';
|
|
5
|
-
|
|
6
|
-
const program = new Command()
|
|
7
|
-
.version(version)
|
|
8
|
-
.option('--nw, --no-write', 'Do NOT write result to file (defaults to false)', false as boolean)
|
|
9
|
-
.option('--ni, --no-inclusions', 'Do NOT process file inclusions (defaults to false)', false as boolean)
|
|
10
|
-
.option('-d, --depopulate', 'Remove populated inclusions from the file', false as boolean)
|
|
11
|
-
.option('-f, --file <flag>', 'The file(s) to process', (value, arr) => (arr.push(value), arr), new Array<string>())
|
|
12
|
-
.option('-r, --remap-imports', 'Remap import specifiers in code blocks from one destination to another')
|
|
13
|
-
.parse();
|
|
14
|
-
|
|
15
|
-
const { inclusions: noInclusions, depopulate, file, write: noWrite } = program.opts();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (file.length === 0) file.push("README.md");
|
|
19
|
-
|
|
20
|
-
/** parkdown: process-order */
|
|
21
|
-
const processors = [
|
|
22
|
-
[populateMarkdownInclusions, !noInclusions],
|
|
23
|
-
[depopulateMarkdownInclusions, depopulate],
|
|
24
|
-
] as const;
|
|
25
|
-
/** parkdown: process-order */
|
|
26
|
-
|
|
27
|
-
for (const [processor] of processors.filter(([_, condition]) => condition))
|
|
28
|
-
for (const _file of file) {
|
|
29
|
-
const result = processor(_file, !noWrite);
|
|
30
|
-
if (noWrite) console.log(result);
|
|
31
|
-
}
|