@canonical/code-standards 0.1.0 → 0.1.2

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.
@@ -89,36 +89,51 @@ cs:YourStandardName a cs:CodeStandard ;
89
89
  cs:name "category/domain/topic" ;
90
90
  cs:hasCategory cs:CategoryName ;
91
91
  cs:description "Clear, concise description of what this standard covers and why it matters." ;
92
- cs:dos """
93
- (Do) First recommended practice with example.
94
- ```code
92
+ cs:do [
93
+ cs:description "First recommended practice." ;
94
+ cs:language "typescript" ;
95
+ cs:code """
95
96
  // Example showing the correct approach
96
- ```
97
-
98
- (Do) Second recommended practice.
99
- ```code
100
- // Another example
101
- ```
102
- """ ;
103
- cs:donts """
104
- (Don't) First anti-pattern to avoid.
105
- ```code
97
+ const good = true;
98
+ """
99
+ ] ;
100
+ cs:do [
101
+ cs:description "Second recommended practice." ;
102
+ cs:language "typescript" ;
103
+ cs:code """
104
+ // Another correct example
105
+ const alsoGood = true;
106
+ """
107
+ ] ;
108
+ cs:dont [
109
+ cs:description "First anti-pattern to avoid." ;
110
+ cs:language "typescript" ;
111
+ cs:code """
106
112
  // Example showing what NOT to do
107
- ```
108
-
109
- (Don't) Second anti-pattern.
110
- ```code
113
+ const bad = true;
114
+ """
115
+ ] ;
116
+ cs:dont [
117
+ cs:description "Second anti-pattern." ;
118
+ cs:language "typescript" ;
119
+ cs:code """
111
120
  // Another bad example
121
+ const alsoBad = true;
122
+ """
123
+ ] .
112
124
  ```
113
- """ .
114
- ```
125
+
126
+ Each `cs:do` and `cs:dont` is a blank node (`cs:Example`) with structured fields:
127
+ - `cs:description` — What the example demonstrates (required)
128
+ - `cs:language` — Language of the code block, e.g. `"typescript"`, `"rust"`, `"css"`, `"bash"`, `"svg"` (optional, omit when no code)
129
+ - `cs:code` — The code content (optional, omit for description-only examples)
115
130
 
116
131
  **Required Properties:**
117
132
  - `cs:name` - Hierarchical identifier (must match NamePattern)
118
133
  - `cs:hasCategory` - Reference to a Category instance
119
134
  - `cs:description` - What and why (plain text or markdown)
120
- - `cs:dos` - What TO do with examples
121
- - `cs:donts` - What NOT to do with examples
135
+ - `cs:do` - One or more positive examples (blank nodes)
136
+ - `cs:dont` - One or more negative examples (blank nodes)
122
137
 
123
138
  **Optional Properties:**
124
139
  - `cs:extends` - Reference to a parent standard this builds upon
@@ -144,7 +159,20 @@ cs:SpecificStandard a cs:CodeStandard ;
144
159
  cs:extends cs:ComponentProps ; # Reference to parent
145
160
  cs:hasCategory cs:ReactCategory ;
146
161
  cs:description "Specific guidance that builds on the general props standard." ;
147
- # ... dos and donts ...
162
+ cs:do [
163
+ cs:description "Example of the specific pattern." ;
164
+ cs:language "typescript" ;
165
+ cs:code """
166
+ // ...
167
+ """
168
+ ] ;
169
+ cs:dont [
170
+ cs:description "Anti-pattern to avoid." ;
171
+ cs:language "typescript" ;
172
+ cs:code """
173
+ // ...
174
+ """
175
+ ] .
148
176
  ```
149
177
 
150
178
  Query to find potential parent standards:
@@ -201,17 +229,19 @@ Add new standards to the file matching their category.
201
229
  - Keep it concise but complete
202
230
  - Use markdown for complex descriptions
203
231
 
204
- ### Do's Guidelines
205
- - Start each item with `(Do)`
232
+ ### Do Examples Guidelines
233
+ - Each `cs:do` blank node is one discrete positive example
234
+ - `cs:description` explains what the example demonstrates
235
+ - `cs:language` must match the code block language (e.g. `"typescript"`, `"css"`, `"rust"`)
236
+ - `cs:code` contains the actual code — no markdown fences needed
206
237
  - Provide concrete, runnable examples
207
238
  - Show the COMPLETE correct pattern
208
- - Explain the reasoning when not obvious
209
239
 
210
- ### Don'ts Guidelines
211
- - Start each item with `(Don't)`
240
+ ### Don't Examples Guidelines
241
+ - Each `cs:dont` blank node is one discrete negative example
212
242
  - Show realistic mistakes developers make
213
- - Explain WHY it's problematic
214
- - Mirror the structure of the do's section
243
+ - `cs:description` should explain WHY it's problematic
244
+ - Mirror the structure of the do examples where possible
215
245
 
216
246
  ### Example Quality Checklist
217
247
  - [ ] Examples are syntactically correct
@@ -231,9 +261,10 @@ cs:ErrorBoundaryUsage a cs:CodeStandard ;
231
261
  cs:name "react/component/error-boundaries" ;
232
262
  cs:hasCategory cs:ReactCategory ;
233
263
  cs:description "Error boundaries must be used to catch JavaScript errors in component trees and display fallback UI. They should be placed strategically to isolate failures without breaking the entire application." ;
234
- cs:dos """
235
- (Do) Wrap feature sections with error boundaries to isolate failures.
236
- ```tsx
264
+ cs:do [
265
+ cs:description "Wrap feature sections with error boundaries to isolate failures." ;
266
+ cs:language "tsx" ;
267
+ cs:code """
237
268
  const Dashboard = () => (
238
269
  <div className="ds dashboard">
239
270
  <ErrorBoundary fallback={<WidgetError />}>
@@ -244,10 +275,12 @@ const Dashboard = () => (
244
275
  </ErrorBoundary>
245
276
  </div>
246
277
  );
247
- ```
248
-
249
- (Do) Provide meaningful fallback UI that helps users understand and recover.
250
- ```tsx
278
+ """
279
+ ] ;
280
+ cs:do [
281
+ cs:description "Provide meaningful fallback UI that helps users understand and recover." ;
282
+ cs:language "tsx" ;
283
+ cs:code """
251
284
  const WidgetError = () => (
252
285
  <div className="ds widget-error">
253
286
  <p>This section couldn't load.</p>
@@ -256,11 +289,12 @@ const WidgetError = () => (
256
289
  </button>
257
290
  </div>
258
291
  );
259
- ```
260
- """ ;
261
- cs:donts """
262
- (Don't) Wrap the entire application in a single error boundary.
263
- ```tsx
292
+ """
293
+ ] ;
294
+ cs:dont [
295
+ cs:description "Wrap the entire application in a single error boundary." ;
296
+ cs:language "tsx" ;
297
+ cs:code """
264
298
  // Bad: One error anywhere crashes everything
265
299
  const App = () => (
266
300
  <ErrorBoundary>
@@ -269,14 +303,16 @@ const App = () => (
269
303
  <Footer />
270
304
  </ErrorBoundary>
271
305
  );
272
- ```
273
-
274
- (Don't) Use generic or unhelpful fallback messages.
275
- ```tsx
306
+ """
307
+ ] ;
308
+ cs:dont [
309
+ cs:description "Use generic or unhelpful fallback messages." ;
310
+ cs:language "tsx" ;
311
+ cs:code """
276
312
  // Bad: Doesn't help the user
277
313
  const fallback = <div>Something went wrong</div>;
278
- ```
279
- """ .
314
+ """
315
+ ] .
280
316
  ```
281
317
 
282
318
  ## Tips
@@ -4,17 +4,23 @@
4
4
  * Usage: bun src/scripts/generate-docs.ts [output-dir]
5
5
  */
6
6
 
7
- import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
8
- import { join, basename } from "path";
7
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { basename, join } from "node:path";
9
9
 
10
10
  const DATA_DIR = join(import.meta.dir, "../../data");
11
11
  const DEFAULT_OUTPUT = "./docs";
12
12
 
13
+ interface Example {
14
+ description: string;
15
+ language: string;
16
+ code: string;
17
+ }
18
+
13
19
  interface Standard {
14
20
  name: string;
15
21
  description: string;
16
- dos: string;
17
- donts: string;
22
+ dos: Example[];
23
+ donts: Example[];
18
24
  }
19
25
 
20
26
  interface Category {
@@ -23,15 +29,15 @@ interface Category {
23
29
  standards: Standard[];
24
30
  }
25
31
 
26
- // Minimal TTL parser - extracts string literals
32
+ // Extract a single string literal (single-line or triple-quoted)
27
33
  function extractLiteral(content: string, predicate: string): string {
28
34
  const regex = new RegExp(`${predicate}\\s+"""([\\s\\S]*?)"""`, "g");
29
35
  const match = regex.exec(content);
30
- if (match) return match[1].trim();
36
+ if (match?.[1]) return match[1].trim();
31
37
 
32
38
  const singleLine = new RegExp(`${predicate}\\s+"([^"]*)"`, "g");
33
39
  const singleMatch = singleLine.exec(content);
34
- return singleMatch ? singleMatch[1].trim() : "";
40
+ return singleMatch?.[1]?.trim() ?? "";
35
41
  }
36
42
 
37
43
  function parseCategory(content: string): { label: string; slug: string } {
@@ -43,6 +49,61 @@ function parseCategory(content: string): { label: string; slug: string } {
43
49
  return { label, slug };
44
50
  }
45
51
 
52
+ /**
53
+ * Parse blank node examples from a standard block for a given predicate (cs:do or cs:dont).
54
+ */
55
+ function parseExamples(
56
+ block: string,
57
+ predicate: "cs:do" | "cs:dont",
58
+ ): Example[] {
59
+ const examples: Example[] = [];
60
+
61
+ // Match blank node patterns: predicate [ ... ]
62
+ // We need to handle nested triple-quoted strings inside the blank node
63
+ const escapedPred = predicate.replace(".", "\\.");
64
+ const regex = new RegExp(`${escapedPred}\\s*\\[`, "g");
65
+ let match = regex.exec(block);
66
+
67
+ while (match !== null) {
68
+ const startIdx = match.index + match[0].length;
69
+
70
+ // Find the matching closing bracket, accounting for nested triple-quoted strings
71
+ let depth = 1;
72
+ let i = startIdx;
73
+ let inTripleQuote = false;
74
+
75
+ while (i < block.length && depth > 0) {
76
+ if (!inTripleQuote) {
77
+ if (block.slice(i, i + 3) === '"""') {
78
+ inTripleQuote = true;
79
+ i += 3;
80
+ continue;
81
+ }
82
+ if (block[i] === "[") depth++;
83
+ if (block[i] === "]") depth--;
84
+ } else {
85
+ if (block.slice(i, i + 3) === '"""') {
86
+ inTripleQuote = false;
87
+ i += 3;
88
+ continue;
89
+ }
90
+ }
91
+ i++;
92
+ }
93
+
94
+ const blankNodeContent = block.slice(startIdx, i - 1);
95
+
96
+ const description = extractLiteral(blankNodeContent, "cs:description");
97
+ const language = extractLiteral(blankNodeContent, "cs:language");
98
+ const code = extractLiteral(blankNodeContent, "cs:code");
99
+
100
+ examples.push({ description, language, code });
101
+ match = regex.exec(block);
102
+ }
103
+
104
+ return examples;
105
+ }
106
+
46
107
  function parseStandards(content: string): Standard[] {
47
108
  // Split by standard definitions
48
109
  const blocks = content.split(/\n(?=\w+:\w+\s+a\s+cs:CodeStandard)/);
@@ -53,8 +114,8 @@ function parseStandards(content: string): Standard[] {
53
114
 
54
115
  const name = extractLiteral(block, "cs:name");
55
116
  const description = extractLiteral(block, "cs:description");
56
- const dos = extractLiteral(block, "cs:dos");
57
- const donts = extractLiteral(block, "cs:donts");
117
+ const dos = parseExamples(block, "cs:do");
118
+ const donts = parseExamples(block, "cs:dont");
58
119
 
59
120
  if (name) {
60
121
  standards.push({ name, description, dos, donts });
@@ -64,6 +125,21 @@ function parseStandards(content: string): Standard[] {
64
125
  return standards.sort((a, b) => a.name.localeCompare(b.name));
65
126
  }
66
127
 
128
+ function renderExample(example: Example): string {
129
+ const parts: string[] = [];
130
+
131
+ if (example.description) {
132
+ parts.push(example.description);
133
+ }
134
+
135
+ if (example.code) {
136
+ const lang = example.language || "";
137
+ parts.push(`\`\`\`${lang}\n${example.code}\n\`\`\``);
138
+ }
139
+
140
+ return parts.join("\n");
141
+ }
142
+
67
143
  function generateMarkdown(category: Category): string {
68
144
  const lines: string[] = [
69
145
  `# ${category.label} Standards`,
@@ -75,11 +151,17 @@ function generateMarkdown(category: Category): string {
75
151
  for (const std of category.standards) {
76
152
  lines.push(`## ${std.name}`, "", std.description, "");
77
153
 
78
- if (std.dos) {
79
- lines.push("### Do", "", std.dos, "");
154
+ if (std.dos.length > 0) {
155
+ lines.push("### Do", "");
156
+ for (const ex of std.dos) {
157
+ lines.push(renderExample(ex), "");
158
+ }
80
159
  }
81
- if (std.donts) {
82
- lines.push("### Don't", "", std.donts, "");
160
+ if (std.donts.length > 0) {
161
+ lines.push("### Don't", "");
162
+ for (const ex of std.donts) {
163
+ lines.push(renderExample(ex), "");
164
+ }
83
165
  }
84
166
  lines.push("---", "");
85
167
  }
@@ -9,11 +9,13 @@ const command = process.argv[2];
9
9
  switch (command) {
10
10
  case "docs":
11
11
  case "generate-docs":
12
- await import("./generate-docs.ts");
12
+ await import("./generate-docs.js");
13
13
  break;
14
14
  default:
15
15
  console.log("Usage: bun src/scripts/index.ts <command>");
16
16
  console.log("");
17
17
  console.log("Commands:");
18
- console.log(" docs [output-dir] Generate markdown docs (default: ./docs)");
18
+ console.log(
19
+ " docs [output-dir] Generate markdown docs (default: ./docs)",
20
+ );
19
21
  }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@canonical/typescript-config",
3
+ "compilerOptions": {
4
+ "noUncheckedIndexedAccess": true,
5
+ "skipLibCheck": true
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }