metascope 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/.DS_Store +0 -0
  2. package/dist/bin/cli.js +14 -14
  3. package/dist/lib/{chunk-DrSxFLj_.js → _virtual/_rolldown/runtime.js} +1 -1
  4. package/dist/lib/file-matching.js +152 -0
  5. package/dist/lib/index.d.ts +11 -1496
  6. package/dist/lib/index.js +6 -6215
  7. package/dist/lib/log.d.ts +11 -0
  8. package/dist/lib/log.js +20 -0
  9. package/dist/lib/metadata-types.d.ts +151 -0
  10. package/dist/lib/metadata-types.js +30 -0
  11. package/dist/lib/metadata.d.ts +16 -0
  12. package/dist/lib/metadata.js +235 -0
  13. package/dist/lib/package.js +5 -0
  14. package/dist/lib/parsers/configparser-parser.js +43 -0
  15. package/dist/lib/parsers/gemspec-parser.js +256 -0
  16. package/dist/lib/parsers/go-mod-parser.js +153 -0
  17. package/dist/lib/parsers/makefile-config-parser.js +102 -0
  18. package/dist/lib/parsers/properties-parser.js +31 -0
  19. package/dist/lib/parsers/rfc822-header-parser.js +48 -0
  20. package/dist/lib/parsers/setup-py-parser.js +173 -0
  21. package/dist/lib/source.d.ts +17 -0
  22. package/dist/lib/source.js +34 -0
  23. package/dist/lib/sources/arduino-library-properties.d.ts +45 -0
  24. package/dist/lib/sources/arduino-library-properties.js +208 -0
  25. package/dist/lib/sources/cinder-cinderblock-xml.d.ts +21 -0
  26. package/dist/lib/sources/cinder-cinderblock-xml.js +134 -0
  27. package/dist/lib/sources/code-stats.d.ts +14 -0
  28. package/dist/lib/sources/code-stats.js +40 -0
  29. package/dist/lib/sources/codemeta-json.d.ts +117 -0
  30. package/dist/lib/sources/codemeta-json.js +226 -0
  31. package/dist/lib/sources/dependency-updates.d.ts +22 -0
  32. package/dist/lib/sources/dependency-updates.js +132 -0
  33. package/dist/lib/sources/file-stats.d.ts +12 -0
  34. package/dist/lib/sources/file-stats.js +48 -0
  35. package/dist/lib/sources/git-config.d.ts +8 -0
  36. package/dist/lib/sources/git-config.js +21 -0
  37. package/dist/lib/sources/git-stats.d.ts +35 -0
  38. package/dist/lib/sources/git-stats.js +130 -0
  39. package/dist/lib/sources/github.d.ts +94 -0
  40. package/dist/lib/sources/github.js +399 -0
  41. package/dist/lib/sources/go-go-mod.d.ts +19 -0
  42. package/dist/lib/sources/go-go-mod.js +38 -0
  43. package/dist/lib/sources/go-goreleaser-yaml.d.ts +19 -0
  44. package/dist/lib/sources/go-goreleaser-yaml.js +152 -0
  45. package/dist/lib/sources/java-pom-xml.d.ts +52 -0
  46. package/dist/lib/sources/java-pom-xml.js +248 -0
  47. package/dist/lib/sources/license-file.d.ts +10 -0
  48. package/dist/lib/sources/license-file.js +26 -0
  49. package/dist/lib/sources/metadata-file.d.ts +14 -0
  50. package/dist/lib/sources/metadata-file.js +109 -0
  51. package/dist/lib/sources/metascope.d.ts +14 -0
  52. package/dist/lib/sources/metascope.js +35 -0
  53. package/dist/lib/sources/node-npm-registry.d.ts +19 -0
  54. package/dist/lib/sources/node-npm-registry.js +74 -0
  55. package/dist/lib/sources/node-package-json.d.ts +7 -0
  56. package/dist/lib/sources/node-package-json.js +27 -0
  57. package/dist/lib/sources/obsidian-plugin-manifest-json.d.ts +17 -0
  58. package/dist/lib/sources/obsidian-plugin-manifest-json.js +34 -0
  59. package/dist/lib/sources/obsidian-plugin-registry.d.ts +10 -0
  60. package/dist/lib/sources/obsidian-plugin-registry.js +44 -0
  61. package/dist/lib/sources/openframeworks-addon-config-mk.d.ts +17 -0
  62. package/dist/lib/sources/openframeworks-addon-config-mk.js +39 -0
  63. package/dist/lib/sources/openframeworks-install-xml.d.ts +20 -0
  64. package/dist/lib/sources/openframeworks-install-xml.js +153 -0
  65. package/dist/lib/sources/processing-library-properties.d.ts +44 -0
  66. package/dist/lib/sources/processing-library-properties.js +219 -0
  67. package/dist/lib/sources/processing-sketch-properties.d.ts +38 -0
  68. package/dist/lib/sources/processing-sketch-properties.js +185 -0
  69. package/dist/lib/sources/publiccode-yaml.d.ts +73 -0
  70. package/dist/lib/sources/publiccode-yaml.js +256 -0
  71. package/dist/lib/sources/python-pkg-info.d.ts +31 -0
  72. package/dist/lib/sources/python-pkg-info.js +115 -0
  73. package/dist/lib/sources/python-pypi-registry.d.ts +19 -0
  74. package/dist/lib/sources/python-pypi-registry.js +101 -0
  75. package/dist/lib/sources/python-pyproject-toml.d.ts +7 -0
  76. package/dist/lib/sources/python-pyproject-toml.js +30 -0
  77. package/dist/lib/sources/python-setup-cfg.d.ts +28 -0
  78. package/dist/lib/sources/python-setup-cfg.js +106 -0
  79. package/dist/lib/sources/python-setup-py.d.ts +28 -0
  80. package/dist/lib/sources/python-setup-py.js +48 -0
  81. package/dist/lib/sources/readme-file.d.ts +11 -0
  82. package/dist/lib/sources/readme-file.js +55 -0
  83. package/dist/lib/sources/ruby-gemspec.d.ts +44 -0
  84. package/dist/lib/sources/ruby-gemspec.js +62 -0
  85. package/dist/lib/sources/rust-cargo-toml.d.ts +40 -0
  86. package/dist/lib/sources/rust-cargo-toml.js +159 -0
  87. package/dist/lib/sources/xcode-info-plist.d.ts +22 -0
  88. package/dist/lib/sources/xcode-info-plist.js +199 -0
  89. package/dist/lib/sources/xcode-project-pbxproj.d.ts +21 -0
  90. package/dist/lib/sources/xcode-project-pbxproj.js +222 -0
  91. package/dist/lib/templates/codemeta.d.ts +47 -0
  92. package/dist/lib/templates/codemeta.js +494 -0
  93. package/dist/lib/templates/frontmatter.d.ts +87 -0
  94. package/dist/lib/templates/frontmatter.js +111 -0
  95. package/dist/lib/templates/index.d.ts +181 -0
  96. package/dist/lib/templates/index.js +22 -0
  97. package/dist/lib/templates/metadata.d.ts +17 -0
  98. package/dist/lib/templates/metadata.js +35 -0
  99. package/dist/lib/templates/project.d.ts +39 -0
  100. package/dist/lib/templates/project.js +51 -0
  101. package/dist/lib/utilities/codemeta-helpers.d.ts +39 -0
  102. package/dist/lib/utilities/codemeta-helpers.js +83 -0
  103. package/dist/lib/utilities/fetch.js +43 -0
  104. package/dist/lib/utilities/formatting.js +28 -0
  105. package/dist/lib/utilities/license-identification.js +141 -0
  106. package/dist/lib/utilities/schema-primitives.js +47 -0
  107. package/dist/lib/utilities/template-helpers.d.ts +135 -0
  108. package/dist/lib/utilities/template-helpers.js +310 -0
  109. package/dist/lib/utilities/tree-sitter-wasm.js +30 -0
  110. package/package.json +6 -6
  111. package/readme.md +62 -15
@@ -0,0 +1,173 @@
1
+ import { splitCommaSeparated } from "../utilities/template-helpers.js";
2
+ import { getPythonLanguage, initParser } from "../utilities/tree-sitter-wasm.js";
3
+ //#region src/lib/parsers/setup-py-parser.ts
4
+ /** Filter nulls from web-tree-sitter's `namedChildren` array. */
5
+ function children(node) {
6
+ return node.namedChildren.filter((c) => c !== null);
7
+ }
8
+ function emptySetupPyData() {
9
+ return {
10
+ author: void 0,
11
+ author_email: void 0,
12
+ classifiers: [],
13
+ description: void 0,
14
+ download_url: void 0,
15
+ extras_require: {},
16
+ install_requires: [],
17
+ keywords: void 0,
18
+ license: void 0,
19
+ long_description: void 0,
20
+ maintainer: void 0,
21
+ maintainer_email: void 0,
22
+ name: void 0,
23
+ project_urls: {},
24
+ python_requires: void 0,
25
+ url: void 0,
26
+ version: void 0
27
+ };
28
+ }
29
+ /** Extract a string literal value from a tree-sitter node. */
30
+ function extractString(node) {
31
+ switch (node.type) {
32
+ case "concatenated_string": {
33
+ const parts = children(node).map((child) => extractString(child)).filter((s) => s !== void 0);
34
+ return parts.length > 0 ? parts.join("") : void 0;
35
+ }
36
+ case "float":
37
+ case "integer": return node.text;
38
+ case "string": {
39
+ const content = children(node).find((c) => c.type === "string_content");
40
+ if (content) return content.text;
41
+ const withoutPrefix = node.text.replace(/^[bfru]*/i, "");
42
+ if (withoutPrefix.startsWith("\"\"\"") || withoutPrefix.startsWith("'''")) return withoutPrefix.slice(3, -3);
43
+ return withoutPrefix.slice(1, -1);
44
+ }
45
+ case "string_content": return node.text;
46
+ default: return;
47
+ }
48
+ }
49
+ /** Extract a list of strings from an array/list literal node. */
50
+ function extractStringList(node) {
51
+ if (node.type === "list") return children(node).map((child) => extractString(child)).filter((s) => s !== void 0);
52
+ if (node.type === "tuple") return children(node).map((child) => extractString(child)).filter((s) => s !== void 0);
53
+ const single = extractString(node);
54
+ return single === void 0 ? [] : [single];
55
+ }
56
+ /** Extract a dict literal into a Record<string, string>. */
57
+ function extractStringDict(node) {
58
+ const result = {};
59
+ if (node.type !== "dictionary") return result;
60
+ for (const pair of children(node)) {
61
+ if (pair.type !== "pair") continue;
62
+ const key = pair.childForFieldName("key");
63
+ const value = pair.childForFieldName("value");
64
+ if (!key || !value) continue;
65
+ const k = extractString(key);
66
+ const v = extractString(value);
67
+ if (k && v) result[k] = v;
68
+ }
69
+ return result;
70
+ }
71
+ /** Extract a dict of string lists (for extras_require). */
72
+ function extractStringListDict(node) {
73
+ const result = {};
74
+ if (node.type !== "dictionary") return result;
75
+ for (const pair of children(node)) {
76
+ if (pair.type !== "pair") continue;
77
+ const key = pair.childForFieldName("key");
78
+ const value = pair.childForFieldName("value");
79
+ if (!key || !value) continue;
80
+ const k = extractString(key);
81
+ if (k) result[k] = extractStringList(value);
82
+ }
83
+ return result;
84
+ }
85
+ /** Simple string attributes to extract from setup() keyword arguments. */
86
+ const STRING_ATTRS = new Set([
87
+ "author",
88
+ "author_email",
89
+ "description",
90
+ "download_url",
91
+ "license",
92
+ "long_description",
93
+ "maintainer",
94
+ "maintainer_email",
95
+ "name",
96
+ "python_requires",
97
+ "url",
98
+ "version"
99
+ ]);
100
+ /**
101
+ * Parse a setup.py file and return structured metadata.
102
+ *
103
+ * Uses tree-sitter with the Python grammar to find the `setup()` call
104
+ * and extract keyword arguments. Only statically determinable values
105
+ * (string/list literals) are extracted — variables and dynamic expressions
106
+ * are skipped.
107
+ */
108
+ async function parseSetupPy(source) {
109
+ const parser = await initParser();
110
+ const python = await getPythonLanguage();
111
+ parser.setLanguage(python);
112
+ const tree = parser.parse(source);
113
+ if (!tree) throw new Error("Failed to parse setup.py source");
114
+ const data = emptySetupPyData();
115
+ const setupCall = findSetupCall(tree.rootNode);
116
+ if (!setupCall) return data;
117
+ const argumentChildren = setupCall.childForFieldName("arguments");
118
+ if (!argumentChildren) return data;
119
+ for (const child of children(argumentChildren)) {
120
+ if (child.type !== "keyword_argument") continue;
121
+ const nameNode = child.childForFieldName("name");
122
+ const valueNode = child.childForFieldName("value");
123
+ if (!nameNode || !valueNode) continue;
124
+ const argumentName = nameNode.text;
125
+ if (STRING_ATTRS.has(argumentName)) {
126
+ const value = extractString(valueNode);
127
+ if (value !== void 0) Object.assign(data, { [argumentName]: value });
128
+ continue;
129
+ }
130
+ switch (argumentName) {
131
+ case "classifiers":
132
+ data.classifiers = extractStringList(valueNode);
133
+ break;
134
+ case "extras_require":
135
+ data.extras_require = extractStringListDict(valueNode);
136
+ break;
137
+ case "install_requires":
138
+ data.install_requires = extractStringList(valueNode);
139
+ break;
140
+ case "keywords":
141
+ if (valueNode.type === "list" || valueNode.type === "tuple") data.keywords = extractStringList(valueNode);
142
+ else {
143
+ const string_ = extractString(valueNode);
144
+ if (string_) data.keywords = splitCommaSeparated(string_);
145
+ }
146
+ break;
147
+ case "project_urls":
148
+ data.project_urls = extractStringDict(valueNode);
149
+ break;
150
+ }
151
+ }
152
+ return data;
153
+ }
154
+ /**
155
+ * Recursively find the setup() or setuptools.setup() call in the AST.
156
+ */
157
+ function findSetupCall(node) {
158
+ if (node.type === "call") {
159
+ const function_ = node.childForFieldName("function");
160
+ if (function_) {
161
+ if (function_.type === "identifier" && function_.text === "setup") return node;
162
+ if (function_.type === "attribute") {
163
+ if (function_.childForFieldName("attribute")?.text === "setup") return node;
164
+ }
165
+ }
166
+ }
167
+ for (const child of children(node)) {
168
+ const result = findSetupCall(child);
169
+ if (result) return result;
170
+ }
171
+ }
172
+ //#endregion
173
+ export { parseSetupPy };
@@ -0,0 +1,17 @@
1
+ //#region src/lib/source.d.ts
2
+ /**
3
+ * A value that is either a single item or an array of items.
4
+ */
5
+ type OneOrMany<T> = T | T[];
6
+ /**
7
+ * A unified record returned by every metadata source.
8
+ * @template D The shape of the primary data extracted from the source.
9
+ * @template E The shape of any additional computed/derived fields.
10
+ */
11
+ type SourceRecord<D extends Record<string, unknown> = Record<string, unknown>, E extends Record<string, unknown> = Record<string, unknown>> = {
12
+ /** Primary structured data from this source. */data: D; /** Additional computed or derived fields not present in the raw source. */
13
+ extra?: E; /** The file path or URL from which the data was derived. */
14
+ source: string;
15
+ };
16
+ //#endregion
17
+ export { OneOrMany, SourceRecord };
@@ -0,0 +1,34 @@
1
+ import { log } from "./log.js";
2
+ import { formatPath } from "./utilities/formatting.js";
3
+ //#region src/lib/source.ts
4
+ /**
5
+ * Define a metadata source with `discover` + `parse`.
6
+ * Automatically wires them into an `extract` implementation that handles:
7
+ * - Empty input check (returns undefined)
8
+ * - Per-input try/catch with log.warn
9
+ * - Filtering undefined results from parse
10
+ * - OneOrMany wrapping (single result unwrapped, multiple as array)
11
+ */
12
+ function defineSource(config) {
13
+ return {
14
+ ...config,
15
+ async extract(context) {
16
+ const inputs = await config.discover(context);
17
+ if (inputs.length === 0) return void 0;
18
+ const results = [];
19
+ for (const input of inputs) try {
20
+ const result = await config.parse(input, context);
21
+ if (result) {
22
+ result.source = formatPath(result.source, context.options.path, context.options.absolute);
23
+ results.push(result);
24
+ }
25
+ } catch (error) {
26
+ log.warn(`Failed to process "${input}": ${error instanceof Error ? error.message : String(error)}`);
27
+ }
28
+ if (results.length === 0) return void 0;
29
+ return results.length === 1 ? results[0] : results;
30
+ }
31
+ };
32
+ }
33
+ //#endregion
34
+ export { defineSource };
@@ -0,0 +1,45 @@
1
+ import { OneOrMany, SourceRecord } from "../source.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/lib/sources/arduino-library-properties.d.ts
5
+ declare const arduinoLibraryPropertiesSchema: z.ZodObject<{
6
+ architectures: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
7
+ authors: z.ZodArray<z.ZodObject<{
8
+ email: z.ZodOptional<z.ZodString>;
9
+ name: z.ZodString;
10
+ }, z.core.$strip>>;
11
+ category: z.ZodOptional<z.ZodEnum<{
12
+ Communication: "Communication";
13
+ "Data Processing": "Data Processing";
14
+ "Data Storage": "Data Storage";
15
+ "Device Control": "Device Control";
16
+ Display: "Display";
17
+ Other: "Other";
18
+ Sensors: "Sensors";
19
+ "Signal Input/Output": "Signal Input/Output";
20
+ Timing: "Timing";
21
+ Uncategorized: "Uncategorized";
22
+ }>>;
23
+ depends: z.ZodArray<z.ZodObject<{
24
+ name: z.ZodString;
25
+ versionConstraint: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>;
27
+ email: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
28
+ includes: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
29
+ license: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
30
+ maintainer: z.ZodOptional<z.ZodObject<{
31
+ email: z.ZodOptional<z.ZodString>;
32
+ name: z.ZodString;
33
+ }, z.core.$strip>>;
34
+ name: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
35
+ paragraph: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
36
+ raw: z.ZodRecord<z.ZodString, z.ZodString>;
37
+ repository: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
38
+ sentence: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
39
+ url: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
40
+ version: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
41
+ }, z.core.$strip>;
42
+ type ArduinoLibraryProperties = z.infer<typeof arduinoLibraryPropertiesSchema>;
43
+ type ArduinoLibraryPropertiesData = OneOrMany<SourceRecord<ArduinoLibraryProperties>> | undefined;
44
+ //#endregion
45
+ export { ArduinoLibraryPropertiesData };
@@ -0,0 +1,208 @@
1
+ import { getMatches } from "../file-matching.js";
2
+ import { parseProperties } from "../parsers/properties-parser.js";
3
+ import { defineSource } from "../source.js";
4
+ import { nonEmptyString, optionalUrl, stringArray } from "../utilities/schema-primitives.js";
5
+ import { splitCommaSeparated } from "../utilities/template-helpers.js";
6
+ import { readFile } from "node:fs/promises";
7
+ import { resolve } from "node:path";
8
+ import { z } from "zod";
9
+ //#region src/lib/sources/arduino-library-properties.ts
10
+ const arduinoLibraryPropertiesPersonEntrySchema = z.object({
11
+ email: z.string().optional(),
12
+ name: z.string()
13
+ });
14
+ const arduinoLibraryPropertiesDependencyEntrySchema = z.object({
15
+ name: z.string(),
16
+ versionConstraint: z.string().optional()
17
+ });
18
+ /** Canonical Arduino library categories. */
19
+ const CANONICAL_CATEGORIES = [
20
+ "Communication",
21
+ "Data Processing",
22
+ "Data Storage",
23
+ "Device Control",
24
+ "Display",
25
+ "Other",
26
+ "Sensors",
27
+ "Signal Input/Output",
28
+ "Timing",
29
+ "Uncategorized"
30
+ ];
31
+ /** Map from letters-only lowercase to canonical category form. */
32
+ const CATEGORY_MAP = new Map(CANONICAL_CATEGORIES.map((cat) => [cat.replaceAll(/[^a-z]/gi, "").toLowerCase(), cat]));
33
+ CATEGORY_MAP.set("sensor", "Sensors");
34
+ const arduinoLibraryPropertiesSchema = z.object({
35
+ architectures: stringArray,
36
+ authors: z.array(arduinoLibraryPropertiesPersonEntrySchema),
37
+ category: z.enum(CANONICAL_CATEGORIES).optional(),
38
+ depends: z.array(arduinoLibraryPropertiesDependencyEntrySchema),
39
+ email: nonEmptyString,
40
+ includes: stringArray,
41
+ license: nonEmptyString,
42
+ maintainer: arduinoLibraryPropertiesPersonEntrySchema.optional(),
43
+ name: nonEmptyString,
44
+ paragraph: nonEmptyString,
45
+ raw: z.record(z.string(), z.string()),
46
+ repository: optionalUrl,
47
+ sentence: nonEmptyString,
48
+ url: optionalUrl,
49
+ version: nonEmptyString
50
+ });
51
+ /**
52
+ * Parse an Arduino `library.properties` content string into a structured object.
53
+ */
54
+ function parse(content) {
55
+ const raw = parseProperties(content);
56
+ const includesValue = get(raw, "includes");
57
+ return arduinoLibraryPropertiesSchema.parse({
58
+ architectures: splitCommaSeparated(get(raw, "architectures") ?? "*"),
59
+ authors: parsePersonList(get(raw, "author") ?? ""),
60
+ category: normalizeCategory(get(raw, "category")),
61
+ depends: parseDependencies(get(raw, "depends") ?? get(raw, "dependencies") ?? ""),
62
+ email: nonEmpty(get(raw, "email")),
63
+ includes: includesValue ? splitCommaSeparated(includesValue) : [],
64
+ license: nonEmpty(get(raw, "license")),
65
+ maintainer: parsePersonList(get(raw, "maintainer") ?? "")[0],
66
+ name: nonEmpty(get(raw, "name")),
67
+ paragraph: nonEmpty(get(raw, "paragraph")),
68
+ raw,
69
+ repository: nonEmpty(get(raw, "repository")),
70
+ sentence: nonEmpty(get(raw, "sentence")),
71
+ url: nonEmpty(get(raw, "url")),
72
+ version: nonEmpty(get(raw, "version"))
73
+ });
74
+ }
75
+ /** Helper to get a value from the raw record. */
76
+ function get(raw, key) {
77
+ return raw[key];
78
+ }
79
+ /** Return a trimmed string, or undefined if empty/whitespace-only. */
80
+ function nonEmpty(value) {
81
+ if (value === void 0) return void 0;
82
+ const trimmed = value.trim();
83
+ return trimmed.length > 0 ? trimmed : void 0;
84
+ }
85
+ /**
86
+ * Normalize a category string to the canonical Arduino category.
87
+ * Strips all non-letter characters, lowercases, and matches against the canonical map.
88
+ * For comma-separated values (e.g. "Sensors, Timing"), takes the first valid match.
89
+ */
90
+ function normalizeCategory(value) {
91
+ if (value === void 0) return void 0;
92
+ const trimmed = value.trim();
93
+ if (trimmed.length === 0) return void 0;
94
+ for (const part of trimmed.split(",")) {
95
+ const key = part.replaceAll(/[^a-z]/gi, "").toLowerCase();
96
+ if (key.length === 0) continue;
97
+ const canonical = CATEGORY_MAP.get(key);
98
+ if (canonical) return canonical;
99
+ }
100
+ }
101
+ /**
102
+ * Parse a comma-separated list of `Name <email>` entries into PersonEntry[].
103
+ */
104
+ function parsePersonList(value) {
105
+ const trimmed = value.trim();
106
+ if (trimmed.length === 0) return [];
107
+ const results = [];
108
+ for (const entry of trimmed.split(",")) {
109
+ const trimmedEntry = entry.trim();
110
+ const bracketIndex = trimmedEntry.indexOf("<");
111
+ if (bracketIndex !== -1) {
112
+ const closeBracket = trimmedEntry.indexOf(">", bracketIndex);
113
+ if (closeBracket !== -1) {
114
+ const name = trimmedEntry.slice(0, bracketIndex).trim();
115
+ const email = trimmedEntry.slice(bracketIndex + 1, closeBracket).trim();
116
+ if (name.length > 0 || email.length > 0) results.push({
117
+ email: email.length > 0 ? email : void 0,
118
+ name
119
+ });
120
+ continue;
121
+ }
122
+ }
123
+ if (trimmedEntry.length > 0) results.push({
124
+ email: void 0,
125
+ name: trimmedEntry
126
+ });
127
+ }
128
+ return results;
129
+ }
130
+ /**
131
+ * Parse a comma-separated list of dependencies with optional version constraints.
132
+ * e.g. "ArduinoHttpClient (>=1.0.0), ArduinoJson" →
133
+ * [{ name: "ArduinoHttpClient", versionConstraint: ">=1.0.0" }, ...]
134
+ */
135
+ function parseDependencies(value) {
136
+ const trimmed = value.trim();
137
+ if (trimmed.length === 0) return [];
138
+ const results = [];
139
+ for (const entry of trimmed.split(",")) {
140
+ const trimmedEntry = entry.trim();
141
+ if (trimmedEntry.length === 0) continue;
142
+ const parenIndex = trimmedEntry.indexOf("(");
143
+ if (parenIndex !== -1) {
144
+ const closeParen = trimmedEntry.indexOf(")", parenIndex);
145
+ if (closeParen !== -1) {
146
+ const name = trimmedEntry.slice(0, parenIndex).trim();
147
+ const constraint = trimmedEntry.slice(parenIndex + 1, closeParen).trim();
148
+ if (name.length > 0) {
149
+ results.push({
150
+ name,
151
+ versionConstraint: constraint.length > 0 ? constraint : void 0
152
+ });
153
+ continue;
154
+ }
155
+ }
156
+ }
157
+ results.push({
158
+ name: trimmedEntry,
159
+ versionConstraint: void 0
160
+ });
161
+ }
162
+ return results;
163
+ }
164
+ /**
165
+ * Arduino-specific fields that distinguish library.properties from
166
+ * Processing's identically-named format.
167
+ */
168
+ const ARDUINO_SPECIFIC_FIELDS = new Set([
169
+ "architectures",
170
+ "depends",
171
+ "dot_a_linkage",
172
+ "maintainer"
173
+ ]);
174
+ /** Processing-exclusive fields that rule out Arduino. */
175
+ const PROCESSING_EXCLUSIVE_FIELDS = new Set([
176
+ "authors",
177
+ "minrevision",
178
+ "prettyversion"
179
+ ]);
180
+ /**
181
+ * Validate that a library.properties file is Arduino (not Processing).
182
+ * Requires name=, version=, author= and either an Arduino-specific field
183
+ * or no Processing-exclusive fields.
184
+ */
185
+ function isArduinoLibraryProperties(content) {
186
+ const raw = parseProperties(content);
187
+ const keys = new Set(Object.keys(raw));
188
+ if (!keys.has("name") || !keys.has("version") || !keys.has("author")) return false;
189
+ for (const field of ARDUINO_SPECIFIC_FIELDS) if (keys.has(field)) return true;
190
+ for (const field of PROCESSING_EXCLUSIVE_FIELDS) if (keys.has(field)) return false;
191
+ return true;
192
+ }
193
+ const arduinoLibraryPropertiesSource = defineSource({
194
+ async discover(context) {
195
+ return getMatches(context.options, ["library.properties"]);
196
+ },
197
+ key: "arduinoLibraryProperties",
198
+ async parse(input, context) {
199
+ const content = await readFile(resolve(context.options.path, input), "utf8");
200
+ if (isArduinoLibraryProperties(content)) return {
201
+ data: parse(content),
202
+ source: input
203
+ };
204
+ },
205
+ phase: 1
206
+ });
207
+ //#endregion
208
+ export { arduinoLibraryPropertiesSource };
@@ -0,0 +1,21 @@
1
+ import { OneOrMany, SourceRecord } from "../source.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/lib/sources/cinder-cinderblock-xml.d.ts
5
+ declare const cinderCinderblockSchema: z.ZodObject<{
6
+ author: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
7
+ git: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
8
+ id: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
9
+ library: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
10
+ license: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
11
+ name: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
12
+ requires: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
13
+ summary: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
14
+ supports: z.ZodPipe<z.ZodTransform<string[], unknown>, z.ZodArray<z.ZodString>>;
15
+ url: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
16
+ version: z.ZodPipe<z.ZodTransform<string | undefined, unknown>, z.ZodOptional<z.ZodString>>;
17
+ }, z.core.$strip>;
18
+ type CinderCinderblock = z.infer<typeof cinderCinderblockSchema>;
19
+ type CinderCinderblockXmlData = OneOrMany<SourceRecord<CinderCinderblock>> | undefined;
20
+ //#endregion
21
+ export { CinderCinderblockXmlData };
@@ -0,0 +1,134 @@
1
+ import { getMatches } from "../file-matching.js";
2
+ import { defineSource } from "../source.js";
3
+ import { nonEmptyString, optionalUrl, stringArray } from "../utilities/schema-primitives.js";
4
+ import { ensureArray, splitCommaSeparated } from "../utilities/template-helpers.js";
5
+ import { readFile } from "node:fs/promises";
6
+ import { resolve } from "node:path";
7
+ import is from "@sindresorhus/is";
8
+ import { z } from "zod";
9
+ import { XMLParser } from "fast-xml-parser";
10
+ //#region src/lib/sources/cinder-cinderblock-xml.ts
11
+ /**
12
+ * Source and parser for Cinder `cinderblock.xml` files.
13
+ *
14
+ * CinderBlock is a package format for the Cinder C++ creative coding framework.
15
+ * Metadata lives primarily in attributes on the `<block>` element, with child
16
+ * elements for OS support (`<supports>`) and dependencies (`<requires>`).
17
+ *
18
+ * Uses `fast-xml-parser` with attribute parsing enabled.
19
+ */
20
+ const cinderCinderblockSchema = z.object({
21
+ author: stringArray,
22
+ git: optionalUrl,
23
+ id: nonEmptyString,
24
+ library: optionalUrl,
25
+ license: nonEmptyString,
26
+ name: nonEmptyString,
27
+ requires: stringArray,
28
+ summary: nonEmptyString,
29
+ supports: stringArray,
30
+ url: optionalUrl,
31
+ version: nonEmptyString
32
+ });
33
+ /**
34
+ * Map CinderBlock OS identifiers to human-readable OS names.
35
+ */
36
+ const OS_MAP = {
37
+ ios: "iOS",
38
+ linux: "Linux",
39
+ macosx: "macOS",
40
+ msw: "Windows"
41
+ };
42
+ /**
43
+ * Parse a `cinderblock.xml` content string into a structured object.
44
+ * Returns undefined if the XML is malformed or missing the expected structure.
45
+ */
46
+ function parse(content) {
47
+ const parser = new XMLParser({
48
+ attributeNamePrefix: "@_",
49
+ ignoreAttributes: false,
50
+ parseTagValue: false
51
+ });
52
+ let data;
53
+ try {
54
+ const parsed = parser.parse(content);
55
+ if (!is.plainObject(parsed)) return void 0;
56
+ data = parsed;
57
+ } catch {
58
+ return;
59
+ }
60
+ if (!is.plainObject(data.cinder)) return void 0;
61
+ const { cinder } = data;
62
+ if (!is.plainObject(cinder.block)) return void 0;
63
+ const { block } = cinder;
64
+ return cinderCinderblockSchema.parse({
65
+ author: splitCommaSeparated(getAttribute(block, "author")),
66
+ git: getAttribute(block, "git"),
67
+ id: getAttribute(block, "id"),
68
+ library: getAttribute(block, "library") ?? getAttribute(block, "libraryUrl"),
69
+ license: getAttribute(block, "license"),
70
+ name: getAttribute(block, "name"),
71
+ requires: parseDependencies(block),
72
+ summary: getAttribute(block, "summary"),
73
+ supports: parseOperatingSystems(block),
74
+ url: getAttribute(block, "url"),
75
+ version: getAttribute(block, "version")
76
+ });
77
+ }
78
+ /**
79
+ * Get a trimmed string attribute from a parsed XML element.
80
+ * fast-xml-parser stores attributes with the `@_` prefix.
81
+ */
82
+ function getAttribute(element, name) {
83
+ const value = element[`@_${name}`];
84
+ if (typeof value !== "string") return void 0;
85
+ const trimmed = value.trim();
86
+ return trimmed.length > 0 ? trimmed : void 0;
87
+ }
88
+ /**
89
+ * Extract deduplicated, mapped operating system names from `<supports os="...">` elements.
90
+ */
91
+ function parseOperatingSystems(block) {
92
+ const results = [];
93
+ const seen = /* @__PURE__ */ new Set();
94
+ for (const support of ensureArray(block.supports)) {
95
+ if (!is.plainObject(support)) continue;
96
+ const os = getAttribute(support, "os");
97
+ if (os) {
98
+ const mapped = OS_MAP[os.toLowerCase()] ?? os;
99
+ if (!seen.has(mapped)) {
100
+ seen.add(mapped);
101
+ results.push(mapped);
102
+ }
103
+ }
104
+ }
105
+ return results;
106
+ }
107
+ /**
108
+ * Extract software dependencies from `<requires>` elements.
109
+ */
110
+ function parseDependencies(block) {
111
+ const results = [];
112
+ for (const dep of ensureArray(block.requires)) {
113
+ if (typeof dep !== "string") continue;
114
+ const trimmed = dep.trim();
115
+ if (trimmed.length > 0) results.push(trimmed);
116
+ }
117
+ return results;
118
+ }
119
+ const cinderCinderblockXmlSource = defineSource({
120
+ async discover(context) {
121
+ return getMatches(context.options, ["cinderblock.xml"]);
122
+ },
123
+ key: "cinderCinderblockXml",
124
+ async parse(input, context) {
125
+ const data = parse(await readFile(resolve(context.options.path, input), "utf8"));
126
+ if (data !== void 0) return {
127
+ data,
128
+ source: input
129
+ };
130
+ },
131
+ phase: 1
132
+ });
133
+ //#endregion
134
+ export { cinderCinderblockXmlSource };
@@ -0,0 +1,14 @@
1
+ import { OneOrMany, SourceRecord } from "../source.js";
2
+ import { Language, LanguageInfo } from "@kitschpatrol/tokei";
3
+
4
+ //#region src/lib/sources/code-stats.d.ts
5
+ type CodeStatsTotals = Omit<LanguageInfo, 'language' | 'reports'> & {
6
+ languages: Language[];
7
+ };
8
+ type CodeStatsFields = {
9
+ /** Per-language line count perLanguage, sorted by lines of code descending. */perLanguage?: LanguageInfo[]; /** Aggregate line counts across all languages. */
10
+ total?: CodeStatsTotals;
11
+ };
12
+ type CodeStatsData = OneOrMany<SourceRecord<CodeStatsFields>> | undefined;
13
+ //#endregion
14
+ export { CodeStatsData };
@@ -0,0 +1,40 @@
1
+ import { log } from "../log.js";
2
+ import { getWorkspaces } from "../file-matching.js";
3
+ import { defineSource } from "../source.js";
4
+ import { tokei } from "@kitschpatrol/tokei";
5
+ //#region src/lib/sources/code-stats.ts
6
+ async function getStatistics(directory, options) {
7
+ const perLanguage = (await tokei({
8
+ include: [directory],
9
+ noIgnore: !options.respectIgnored,
10
+ noIgnoreDot: !options.respectIgnored,
11
+ noIgnoreVcs: !options.respectIgnored
12
+ })).toSorted((a, b) => b.code - a.code);
13
+ return {
14
+ data: {
15
+ perLanguage,
16
+ total: {
17
+ blanks: perLanguage.reduce((sum, entry) => sum + entry.blanks, 0),
18
+ code: perLanguage.reduce((sum, entry) => sum + entry.code, 0),
19
+ comments: perLanguage.reduce((sum, entry) => sum + entry.comments, 0),
20
+ files: perLanguage.reduce((sum, entry) => sum + entry.files, 0),
21
+ languages: perLanguage.map((entry) => entry.language),
22
+ lines: perLanguage.reduce((sum, entry) => sum + entry.lines, 0)
23
+ }
24
+ },
25
+ source: directory
26
+ };
27
+ }
28
+ const codeStatsSource = defineSource({
29
+ async discover(context) {
30
+ return [context.options.path, ...getWorkspaces(context.options.path, context.options.workspaces)];
31
+ },
32
+ key: "codeStats",
33
+ async parse(input, context) {
34
+ log.debug("Extracting lines of code via tokei...");
35
+ return getStatistics(input, context.options);
36
+ },
37
+ phase: 1
38
+ });
39
+ //#endregion
40
+ export { codeStatsSource };