@hyperfixi/patterns-reference 2.9.1 → 2.9.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.
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "meta": {
3
3
  "bar": {
4
- "lokascript": "compileSync(code).ok via @hyperfixi/core (same call as the browser _= path), with @hyperfixi/reactivity and @hyperfixi/realtime installed (both ship pre-installed in hyperfixi.js), plus a jsdom top-level install smoke (no error within 500ms settle window)",
4
+ "lokascript": "compileSync(code).ok with zero recovered errors via @hyperfixi/core (same call as the browser _= path), with @hyperfixi/reactivity and @hyperfixi/realtime installed (both ship pre-installed in hyperfixi.js), plus a jsdom top-level install smoke (no error within 500ms settle window)",
5
5
  "hyperscript": "upstream _hyperscript parse with zero recovered errors (extensions loaded: socket, worker, eventsource); parse-level only",
6
6
  "html": "HTML-markup patterns: each _= / script-tag snippet verified individually; hx-live/sse-*/ws-* attributes are hyperfixi-only and block the upstream claim"
7
7
  },
8
- "lokascriptVersion": "2.6.0",
8
+ "lokascriptVersion": "2.9.1",
9
9
  "hyperscriptVersion": "0.9.93"
10
10
  },
11
11
  "engines": {
@@ -129,7 +129,7 @@
129
129
  "send-event-to-form": "both",
130
130
  "send-with-detail": "both",
131
131
  "set-attribute": "both",
132
- "set-color-variable": "lokascript",
132
+ "set-color-variable": "both",
133
133
  "set-inner-html-possessive-dot": "both",
134
134
  "set-opacity": "both",
135
135
  "set-style": "both",
@@ -677,8 +677,7 @@
677
677
  },
678
678
  "set-color-variable": {
679
679
  "lokascript": true,
680
- "hyperscript": false,
681
- "hyperscriptError": "Unexpected value: *"
680
+ "hyperscript": true
682
681
  },
683
682
  "set-inner-html-possessive-dot": {
684
683
  "lokascript": true,
package/dist/index.d.mts CHANGED
@@ -419,6 +419,77 @@ declare function queryPatternsForCommand(command: string, language?: string): Pr
419
419
  */
420
420
  declare function getSupportedLanguages(): Promise<string[]>;
421
421
 
422
+ /**
423
+ * Extract hyperscript sources out of HTML markup, via the DOM.
424
+ *
425
+ * Shared by the two harnesses that need it: `scripts/verify-engines.ts` (which
426
+ * classifies each corpus pattern's `engine` column) and the testing-framework's
427
+ * shipped-sources validity gate.
428
+ *
429
+ * **Why the DOM and not a regex.** A raw-text regex over `.html` cannot tell a
430
+ * live attribute from a code sample: a `<pre>`/`<code>`/`.code` block showing
431
+ * escaped source (`&lt;button _="…"&gt;`) matches just as well as the real
432
+ * thing. That is not hypothetical — it is how an earlier sweep "found" two
433
+ * malformed sources in `examples/events-and-dom/send-events.html` that were
434
+ * only ever display text. Parsing the markup makes the distinction structural:
435
+ * escaped text decodes into a text node, never into an attribute, so it cannot
436
+ * be reached by an attribute walk.
437
+ *
438
+ * Do not reuse `@hyperfixi/vite-plugin`'s scanner for this purpose — it is
439
+ * deliberately a regex (it scans partial source files that need not be
440
+ * well-formed HTML) and has exactly that blind spot.
441
+ */
442
+ /**
443
+ * The DOM surface this module needs, declared structurally.
444
+ *
445
+ * This package compiles with `lib: ["ES2022"]` (it is Node-oriented — a SQLite
446
+ * pattern database), so the ambient `Document`/`Element` types are not in
447
+ * scope. Declaring the three members used here keeps the extractor usable from
448
+ * a jsdom window, a browser document, or a happy-dom one without dragging the
449
+ * whole DOM lib into the package's build.
450
+ */
451
+ interface MinimalAttr {
452
+ name: string;
453
+ value: string;
454
+ }
455
+ interface MinimalElement {
456
+ tagName: string;
457
+ attributes: ArrayLike<MinimalAttr>;
458
+ textContent: string | null;
459
+ getAttribute(name: string): string | null;
460
+ }
461
+ interface MinimalContainer extends MinimalElement {
462
+ innerHTML: string;
463
+ querySelectorAll(selectors: string): ArrayLike<MinimalElement>;
464
+ }
465
+ interface MinimalDocument {
466
+ createElement(tagName: string): MinimalContainer;
467
+ }
468
+ interface MarkupSnippets {
469
+ /** Every hyperscript source found, in document order. */
470
+ snippets: string[];
471
+ /**
472
+ * The markup uses at least one attribute upstream `_hyperscript` has no
473
+ * concept of, so it cannot be claimed as upstream-compatible regardless of
474
+ * how its snippets parse.
475
+ */
476
+ hyperfixiOnly: boolean;
477
+ }
478
+ /**
479
+ * Pull every hyperscript source out of `markup`.
480
+ *
481
+ * Collects `_="…"` attributes, `hx-live="…"` values (which ARE hyperscript —
482
+ * the htmx-compat layer compiles them), and `<script type="text/hyperscript">`
483
+ * bodies. The other hyperfixi-only attribute values (`sse-connect` URLs,
484
+ * `sse-swap` event names, `ws-connect` URLs) are not hyperscript and carry no
485
+ * snippet to verify, but they do set `hyperfixiOnly`.
486
+ *
487
+ * `doc` is any DOM `Document` — a jsdom window's, or the ambient one in a
488
+ * browser/jsdom test environment. Markup that fails to parse yields no
489
+ * snippets rather than throwing.
490
+ */
491
+ declare function extractHyperscriptFromMarkup(doc: MinimalDocument, markup: string): MarkupSnippets;
492
+
422
493
  /**
423
494
  * @hyperfixi/patterns-reference
424
495
  *
@@ -456,9 +527,10 @@ declare function getSupportedLanguages(): Promise<string[]>;
456
527
  * @returns PatternsReference instance
457
528
  */
458
529
  declare function createPatternsReference(options?: ConnectionOptions): PatternsReference;
530
+
459
531
  /**
460
532
  * Version of the package.
461
533
  */
462
534
  declare const VERSION = "0.1.0";
463
535
 
464
- export { type BehaviorRuntime, Command, ConnectionOptions, DatabasePatternsProvider, Expression, Feature, type IntegrationResult, Keyword, LLMExample, type LLMExampleRecord, LanguageDocsStats, LanguageElement, LanguageElementType, type LoadBehaviorsOptions, type LoadBehaviorsResult, type PatternEntry, PatternsReference, type PatternsSource, type SemanticIntegrationOptions, SpecialSymbol, VERSION, buildFewShotContextSync, closeDatabase, createLLMAdapter, createPatternsProvider, createPatternsReference, findExamplesByCommand, findRelevantExamples, getAllCommands, getAllExpressions, getAllFeatures, getAllKeywords, getAllSpecialSymbols, getBridgeProvider, getCommandByName, getCurrentDbPath, getDatabase, getDefaultDbPath, getDefaultProvider, getExpressionByName, getExpressionsByCategory, getFeatureByName, getKeywordByName, getLLMExampleStats, getLanguageDocsStats, getSpecialSymbolByName, getSupportedLanguages, initializeSemanticIntegration, isConnected, isDatabaseAvailable, isSemanticIntegrationInitialized, loadBehaviors, queryPatterns, queryPatternsForCommand, resetConnection, resetDefaultProvider, searchCommands, searchLanguageElements, trackExampleUsage, uninitializeSemanticIntegration };
536
+ export { type BehaviorRuntime, Command, ConnectionOptions, DatabasePatternsProvider, Expression, Feature, type IntegrationResult, Keyword, LLMExample, type LLMExampleRecord, LanguageDocsStats, LanguageElement, LanguageElementType, type LoadBehaviorsOptions, type LoadBehaviorsResult, type MarkupSnippets, type PatternEntry, PatternsReference, type PatternsSource, type SemanticIntegrationOptions, SpecialSymbol, VERSION, buildFewShotContextSync, closeDatabase, createLLMAdapter, createPatternsProvider, createPatternsReference, extractHyperscriptFromMarkup, findExamplesByCommand, findRelevantExamples, getAllCommands, getAllExpressions, getAllFeatures, getAllKeywords, getAllSpecialSymbols, getBridgeProvider, getCommandByName, getCurrentDbPath, getDatabase, getDefaultDbPath, getDefaultProvider, getExpressionByName, getExpressionsByCategory, getFeatureByName, getKeywordByName, getLLMExampleStats, getLanguageDocsStats, getSpecialSymbolByName, getSupportedLanguages, initializeSemanticIntegration, isConnected, isDatabaseAvailable, isSemanticIntegrationInitialized, loadBehaviors, queryPatterns, queryPatternsForCommand, resetConnection, resetDefaultProvider, searchCommands, searchLanguageElements, trackExampleUsage, uninitializeSemanticIntegration };
package/dist/index.d.ts CHANGED
@@ -419,6 +419,77 @@ declare function queryPatternsForCommand(command: string, language?: string): Pr
419
419
  */
420
420
  declare function getSupportedLanguages(): Promise<string[]>;
421
421
 
422
+ /**
423
+ * Extract hyperscript sources out of HTML markup, via the DOM.
424
+ *
425
+ * Shared by the two harnesses that need it: `scripts/verify-engines.ts` (which
426
+ * classifies each corpus pattern's `engine` column) and the testing-framework's
427
+ * shipped-sources validity gate.
428
+ *
429
+ * **Why the DOM and not a regex.** A raw-text regex over `.html` cannot tell a
430
+ * live attribute from a code sample: a `<pre>`/`<code>`/`.code` block showing
431
+ * escaped source (`&lt;button _="…"&gt;`) matches just as well as the real
432
+ * thing. That is not hypothetical — it is how an earlier sweep "found" two
433
+ * malformed sources in `examples/events-and-dom/send-events.html` that were
434
+ * only ever display text. Parsing the markup makes the distinction structural:
435
+ * escaped text decodes into a text node, never into an attribute, so it cannot
436
+ * be reached by an attribute walk.
437
+ *
438
+ * Do not reuse `@hyperfixi/vite-plugin`'s scanner for this purpose — it is
439
+ * deliberately a regex (it scans partial source files that need not be
440
+ * well-formed HTML) and has exactly that blind spot.
441
+ */
442
+ /**
443
+ * The DOM surface this module needs, declared structurally.
444
+ *
445
+ * This package compiles with `lib: ["ES2022"]` (it is Node-oriented — a SQLite
446
+ * pattern database), so the ambient `Document`/`Element` types are not in
447
+ * scope. Declaring the three members used here keeps the extractor usable from
448
+ * a jsdom window, a browser document, or a happy-dom one without dragging the
449
+ * whole DOM lib into the package's build.
450
+ */
451
+ interface MinimalAttr {
452
+ name: string;
453
+ value: string;
454
+ }
455
+ interface MinimalElement {
456
+ tagName: string;
457
+ attributes: ArrayLike<MinimalAttr>;
458
+ textContent: string | null;
459
+ getAttribute(name: string): string | null;
460
+ }
461
+ interface MinimalContainer extends MinimalElement {
462
+ innerHTML: string;
463
+ querySelectorAll(selectors: string): ArrayLike<MinimalElement>;
464
+ }
465
+ interface MinimalDocument {
466
+ createElement(tagName: string): MinimalContainer;
467
+ }
468
+ interface MarkupSnippets {
469
+ /** Every hyperscript source found, in document order. */
470
+ snippets: string[];
471
+ /**
472
+ * The markup uses at least one attribute upstream `_hyperscript` has no
473
+ * concept of, so it cannot be claimed as upstream-compatible regardless of
474
+ * how its snippets parse.
475
+ */
476
+ hyperfixiOnly: boolean;
477
+ }
478
+ /**
479
+ * Pull every hyperscript source out of `markup`.
480
+ *
481
+ * Collects `_="…"` attributes, `hx-live="…"` values (which ARE hyperscript —
482
+ * the htmx-compat layer compiles them), and `<script type="text/hyperscript">`
483
+ * bodies. The other hyperfixi-only attribute values (`sse-connect` URLs,
484
+ * `sse-swap` event names, `ws-connect` URLs) are not hyperscript and carry no
485
+ * snippet to verify, but they do set `hyperfixiOnly`.
486
+ *
487
+ * `doc` is any DOM `Document` — a jsdom window's, or the ambient one in a
488
+ * browser/jsdom test environment. Markup that fails to parse yields no
489
+ * snippets rather than throwing.
490
+ */
491
+ declare function extractHyperscriptFromMarkup(doc: MinimalDocument, markup: string): MarkupSnippets;
492
+
422
493
  /**
423
494
  * @hyperfixi/patterns-reference
424
495
  *
@@ -456,9 +527,10 @@ declare function getSupportedLanguages(): Promise<string[]>;
456
527
  * @returns PatternsReference instance
457
528
  */
458
529
  declare function createPatternsReference(options?: ConnectionOptions): PatternsReference;
530
+
459
531
  /**
460
532
  * Version of the package.
461
533
  */
462
534
  declare const VERSION = "0.1.0";
463
535
 
464
- export { type BehaviorRuntime, Command, ConnectionOptions, DatabasePatternsProvider, Expression, Feature, type IntegrationResult, Keyword, LLMExample, type LLMExampleRecord, LanguageDocsStats, LanguageElement, LanguageElementType, type LoadBehaviorsOptions, type LoadBehaviorsResult, type PatternEntry, PatternsReference, type PatternsSource, type SemanticIntegrationOptions, SpecialSymbol, VERSION, buildFewShotContextSync, closeDatabase, createLLMAdapter, createPatternsProvider, createPatternsReference, findExamplesByCommand, findRelevantExamples, getAllCommands, getAllExpressions, getAllFeatures, getAllKeywords, getAllSpecialSymbols, getBridgeProvider, getCommandByName, getCurrentDbPath, getDatabase, getDefaultDbPath, getDefaultProvider, getExpressionByName, getExpressionsByCategory, getFeatureByName, getKeywordByName, getLLMExampleStats, getLanguageDocsStats, getSpecialSymbolByName, getSupportedLanguages, initializeSemanticIntegration, isConnected, isDatabaseAvailable, isSemanticIntegrationInitialized, loadBehaviors, queryPatterns, queryPatternsForCommand, resetConnection, resetDefaultProvider, searchCommands, searchLanguageElements, trackExampleUsage, uninitializeSemanticIntegration };
536
+ export { type BehaviorRuntime, Command, ConnectionOptions, DatabasePatternsProvider, Expression, Feature, type IntegrationResult, Keyword, LLMExample, type LLMExampleRecord, LanguageDocsStats, LanguageElement, LanguageElementType, type LoadBehaviorsOptions, type LoadBehaviorsResult, type MarkupSnippets, type PatternEntry, PatternsReference, type PatternsSource, type SemanticIntegrationOptions, SpecialSymbol, VERSION, buildFewShotContextSync, closeDatabase, createLLMAdapter, createPatternsProvider, createPatternsReference, extractHyperscriptFromMarkup, findExamplesByCommand, findRelevantExamples, getAllCommands, getAllExpressions, getAllFeatures, getAllKeywords, getAllSpecialSymbols, getBridgeProvider, getCommandByName, getCurrentDbPath, getDatabase, getDefaultDbPath, getDefaultProvider, getExpressionByName, getExpressionsByCategory, getFeatureByName, getKeywordByName, getLLMExampleStats, getLanguageDocsStats, getSpecialSymbolByName, getSupportedLanguages, initializeSemanticIntegration, isConnected, isDatabaseAvailable, isSemanticIntegrationInitialized, loadBehaviors, queryPatterns, queryPatternsForCommand, resetConnection, resetDefaultProvider, searchCommands, searchLanguageElements, trackExampleUsage, uninitializeSemanticIntegration };
package/dist/index.js CHANGED
@@ -1347,6 +1347,41 @@ async function getSupportedLanguages() {
1347
1347
  return provider.getSupportedLanguages();
1348
1348
  }
1349
1349
 
1350
+ // src/html-snippets.ts
1351
+ var HYPERFIXI_ONLY_ATTR_PREFIXES = ["hx-live", "sse-", "ws-"];
1352
+ function extractHyperscriptFromMarkup(doc, markup) {
1353
+ const container = doc.createElement("div");
1354
+ try {
1355
+ container.innerHTML = markup;
1356
+ } catch {
1357
+ return { snippets: [], hyperfixiOnly: false };
1358
+ }
1359
+ const snippets = [];
1360
+ let hyperfixiOnly = false;
1361
+ for (const el of Array.from(container.querySelectorAll("*"))) {
1362
+ for (const attr of Array.from(el.attributes)) {
1363
+ if (attr.name === "_" && attr.value.trim()) {
1364
+ snippets.push(attr.value);
1365
+ }
1366
+ if (attr.name === "hx-live" && attr.value.trim()) {
1367
+ snippets.push(attr.value);
1368
+ }
1369
+ if (HYPERFIXI_ONLY_ATTR_PREFIXES.some((p) => attr.name.startsWith(p))) {
1370
+ hyperfixiOnly = true;
1371
+ }
1372
+ }
1373
+ if (el.tagName === "SCRIPT") {
1374
+ const type = el.getAttribute("type");
1375
+ if (type === "text/hyperscript-template") {
1376
+ hyperfixiOnly = true;
1377
+ } else if (type === "text/hyperscript" && el.textContent?.trim()) {
1378
+ snippets.push(el.textContent);
1379
+ }
1380
+ }
1381
+ }
1382
+ return { snippets, hyperfixiOnly };
1383
+ }
1384
+
1350
1385
  // src/index.ts
1351
1386
  function createPatternsReference(options) {
1352
1387
  getDatabase(options);
@@ -1383,6 +1418,7 @@ exports.createPatternsProvider = createPatternsProvider;
1383
1418
  exports.createPatternsReference = createPatternsReference;
1384
1419
  exports.dbStampPath = dbStampPath;
1385
1420
  exports.discoverPatterns = discoverPatterns;
1421
+ exports.extractHyperscriptFromMarkup = extractHyperscriptFromMarkup;
1386
1422
  exports.findExamplesByCommand = findExamplesByCommand;
1387
1423
  exports.findRelevantExamples = findRelevantExamples;
1388
1424
  exports.getAllCommands = getAllCommands;
package/dist/index.mjs CHANGED
@@ -1339,6 +1339,41 @@ async function getSupportedLanguages() {
1339
1339
  return provider.getSupportedLanguages();
1340
1340
  }
1341
1341
 
1342
+ // src/html-snippets.ts
1343
+ var HYPERFIXI_ONLY_ATTR_PREFIXES = ["hx-live", "sse-", "ws-"];
1344
+ function extractHyperscriptFromMarkup(doc, markup) {
1345
+ const container = doc.createElement("div");
1346
+ try {
1347
+ container.innerHTML = markup;
1348
+ } catch {
1349
+ return { snippets: [], hyperfixiOnly: false };
1350
+ }
1351
+ const snippets = [];
1352
+ let hyperfixiOnly = false;
1353
+ for (const el of Array.from(container.querySelectorAll("*"))) {
1354
+ for (const attr of Array.from(el.attributes)) {
1355
+ if (attr.name === "_" && attr.value.trim()) {
1356
+ snippets.push(attr.value);
1357
+ }
1358
+ if (attr.name === "hx-live" && attr.value.trim()) {
1359
+ snippets.push(attr.value);
1360
+ }
1361
+ if (HYPERFIXI_ONLY_ATTR_PREFIXES.some((p) => attr.name.startsWith(p))) {
1362
+ hyperfixiOnly = true;
1363
+ }
1364
+ }
1365
+ if (el.tagName === "SCRIPT") {
1366
+ const type = el.getAttribute("type");
1367
+ if (type === "text/hyperscript-template") {
1368
+ hyperfixiOnly = true;
1369
+ } else if (type === "text/hyperscript" && el.textContent?.trim()) {
1370
+ snippets.push(el.textContent);
1371
+ }
1372
+ }
1373
+ }
1374
+ return { snippets, hyperfixiOnly };
1375
+ }
1376
+
1342
1377
  // src/index.ts
1343
1378
  function createPatternsReference(options) {
1344
1379
  getDatabase(options);
@@ -1362,4 +1397,4 @@ function createPatternsReference(options) {
1362
1397
  }
1363
1398
  var VERSION = "0.1.0";
1364
1399
 
1365
- export { DatabasePatternsProvider, VERSION, addLLMExample, buildFewShotContext, buildFewShotContextSync, checkDbStamp, closeDatabase, computeDbInputHash, createLLMAdapter, createPatternsProvider, createPatternsReference, dbStampPath, discoverPatterns, findExamplesByCommand, findRelevantExamples, getAllCommands, getAllExpressions, getAllFeatures, getAllKeywords, getAllPatterns, getAllSpecialSymbols, getAllTranslations, getBehaviorPatterns, getBridgeProvider, getCommandByName, getCurrentDbPath, getDatabase, getDefaultDbPath, getDefaultProvider, getExamplesByCommand, getExpressionByName, getExpressionsByCategory, getFeatureByName, getHighConfidenceTranslations, getHighQualityExamples, getKeywordByName, getLLMExampleStats, getLLMExamples, getLLMStats, getLanguageDocsStats, getMostUsedExamples, getPatternById, getPatternStats, getPatternsByCategory, getPatternsByCommand, getSpecialSymbolByName, getSupportedLanguages, getTranslation, getTranslationStats, getTranslationsByLanguage, getVerifiedTranslations, getWordOrder, initializeSemanticIntegration, isConnected, isDatabaseAvailable, isSemanticIntegrationInitialized, loadBehaviors, queryPatterns, queryPatternsForCommand, resetConnection, resetDefaultProvider, searchCommands, searchLanguageElements, searchPatterns, seedLLMExamples, syncTranslations, trackExampleUsage, uninitializeSemanticIntegration, updateQualityScore, validateAllTranslations, verifyTranslation, writeDbStamp };
1400
+ export { DatabasePatternsProvider, VERSION, addLLMExample, buildFewShotContext, buildFewShotContextSync, checkDbStamp, closeDatabase, computeDbInputHash, createLLMAdapter, createPatternsProvider, createPatternsReference, dbStampPath, discoverPatterns, extractHyperscriptFromMarkup, findExamplesByCommand, findRelevantExamples, getAllCommands, getAllExpressions, getAllFeatures, getAllKeywords, getAllPatterns, getAllSpecialSymbols, getAllTranslations, getBehaviorPatterns, getBridgeProvider, getCommandByName, getCurrentDbPath, getDatabase, getDefaultDbPath, getDefaultProvider, getExamplesByCommand, getExpressionByName, getExpressionsByCategory, getFeatureByName, getHighConfidenceTranslations, getHighQualityExamples, getKeywordByName, getLLMExampleStats, getLLMExamples, getLLMStats, getLanguageDocsStats, getMostUsedExamples, getPatternById, getPatternStats, getPatternsByCategory, getPatternsByCommand, getSpecialSymbolByName, getSupportedLanguages, getTranslation, getTranslationStats, getTranslationsByLanguage, getVerifiedTranslations, getWordOrder, initializeSemanticIntegration, isConnected, isDatabaseAvailable, isSemanticIntegrationInitialized, loadBehaviors, queryPatterns, queryPatternsForCommand, resetConnection, resetDefaultProvider, searchCommands, searchLanguageElements, searchPatterns, seedLLMExamples, syncTranslations, trackExampleUsage, uninitializeSemanticIntegration, updateQualityScore, validateAllTranslations, verifyTranslation, writeDbStamp };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperfixi/patterns-reference",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
4
4
  "description": "Queryable patterns database for hyperscript with multilingual translations and LLM support",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -66,13 +66,13 @@
66
66
  },
67
67
  "homepage": "https://github.com/codetalcott/hyperfixi/tree/main/packages/patterns-reference#readme",
68
68
  "dependencies": {
69
- "@lokascript/semantic": "^2.9.1",
69
+ "@lokascript/semantic": "^2.9.3",
70
70
  "better-sqlite3": "^12.11.1"
71
71
  },
72
72
  "devDependencies": {
73
- "@hyperfixi/core": "^2.9.1",
74
- "@hyperfixi/reactivity": "^2.9.1",
75
- "@hyperfixi/realtime": "^2.9.1",
73
+ "@hyperfixi/core": "^2.9.3",
74
+ "@hyperfixi/reactivity": "^2.9.3",
75
+ "@hyperfixi/realtime": "^2.9.3",
76
76
  "@types/better-sqlite3": "^7.6.0",
77
77
  "@types/node": "^20.0.0",
78
78
  "hyperscript.org": "0.9.93",
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Extract hyperscript sources out of HTML markup, via the DOM.
3
+ *
4
+ * Shared by the two harnesses that need it: `scripts/verify-engines.ts` (which
5
+ * classifies each corpus pattern's `engine` column) and the testing-framework's
6
+ * shipped-sources validity gate.
7
+ *
8
+ * **Why the DOM and not a regex.** A raw-text regex over `.html` cannot tell a
9
+ * live attribute from a code sample: a `<pre>`/`<code>`/`.code` block showing
10
+ * escaped source (`&lt;button _="…"&gt;`) matches just as well as the real
11
+ * thing. That is not hypothetical — it is how an earlier sweep "found" two
12
+ * malformed sources in `examples/events-and-dom/send-events.html` that were
13
+ * only ever display text. Parsing the markup makes the distinction structural:
14
+ * escaped text decodes into a text node, never into an attribute, so it cannot
15
+ * be reached by an attribute walk.
16
+ *
17
+ * Do not reuse `@hyperfixi/vite-plugin`'s scanner for this purpose — it is
18
+ * deliberately a regex (it scans partial source files that need not be
19
+ * well-formed HTML) and has exactly that blind spot.
20
+ */
21
+
22
+ /** Attribute prefixes that only the hyperfixi htmx-compat layer implements. */
23
+ const HYPERFIXI_ONLY_ATTR_PREFIXES = ['hx-live', 'sse-', 'ws-'];
24
+
25
+ /**
26
+ * The DOM surface this module needs, declared structurally.
27
+ *
28
+ * This package compiles with `lib: ["ES2022"]` (it is Node-oriented — a SQLite
29
+ * pattern database), so the ambient `Document`/`Element` types are not in
30
+ * scope. Declaring the three members used here keeps the extractor usable from
31
+ * a jsdom window, a browser document, or a happy-dom one without dragging the
32
+ * whole DOM lib into the package's build.
33
+ */
34
+ export interface MinimalAttr {
35
+ name: string;
36
+ value: string;
37
+ }
38
+
39
+ export interface MinimalElement {
40
+ tagName: string;
41
+ attributes: ArrayLike<MinimalAttr>;
42
+ textContent: string | null;
43
+ getAttribute(name: string): string | null;
44
+ }
45
+
46
+ export interface MinimalContainer extends MinimalElement {
47
+ innerHTML: string;
48
+ querySelectorAll(selectors: string): ArrayLike<MinimalElement>;
49
+ }
50
+
51
+ export interface MinimalDocument {
52
+ createElement(tagName: string): MinimalContainer;
53
+ }
54
+
55
+ export interface MarkupSnippets {
56
+ /** Every hyperscript source found, in document order. */
57
+ snippets: string[];
58
+ /**
59
+ * The markup uses at least one attribute upstream `_hyperscript` has no
60
+ * concept of, so it cannot be claimed as upstream-compatible regardless of
61
+ * how its snippets parse.
62
+ */
63
+ hyperfixiOnly: boolean;
64
+ }
65
+
66
+ /**
67
+ * Pull every hyperscript source out of `markup`.
68
+ *
69
+ * Collects `_="…"` attributes, `hx-live="…"` values (which ARE hyperscript —
70
+ * the htmx-compat layer compiles them), and `<script type="text/hyperscript">`
71
+ * bodies. The other hyperfixi-only attribute values (`sse-connect` URLs,
72
+ * `sse-swap` event names, `ws-connect` URLs) are not hyperscript and carry no
73
+ * snippet to verify, but they do set `hyperfixiOnly`.
74
+ *
75
+ * `doc` is any DOM `Document` — a jsdom window's, or the ambient one in a
76
+ * browser/jsdom test environment. Markup that fails to parse yields no
77
+ * snippets rather than throwing.
78
+ */
79
+ export function extractHyperscriptFromMarkup(doc: MinimalDocument, markup: string): MarkupSnippets {
80
+ const container = doc.createElement('div');
81
+ try {
82
+ container.innerHTML = markup;
83
+ } catch {
84
+ return { snippets: [], hyperfixiOnly: false };
85
+ }
86
+
87
+ const snippets: string[] = [];
88
+ let hyperfixiOnly = false;
89
+
90
+ for (const el of Array.from(container.querySelectorAll('*'))) {
91
+ for (const attr of Array.from(el.attributes)) {
92
+ if (attr.name === '_' && attr.value.trim()) {
93
+ snippets.push(attr.value);
94
+ }
95
+ if (attr.name === 'hx-live' && attr.value.trim()) {
96
+ snippets.push(attr.value);
97
+ }
98
+ if (HYPERFIXI_ONLY_ATTR_PREFIXES.some(p => attr.name.startsWith(p))) {
99
+ hyperfixiOnly = true;
100
+ }
101
+ }
102
+
103
+ if (el.tagName === 'SCRIPT') {
104
+ const type = el.getAttribute('type');
105
+ if (type === 'text/hyperscript-template') {
106
+ hyperfixiOnly = true;
107
+ } else if (type === 'text/hyperscript' && el.textContent?.trim()) {
108
+ snippets.push(el.textContent);
109
+ }
110
+ }
111
+ }
112
+
113
+ return { snippets, hyperfixiOnly };
114
+ }
package/src/index.ts CHANGED
@@ -275,6 +275,14 @@ export function createPatternsReference(options?: ConnectionOptions): PatternsRe
275
275
  };
276
276
  }
277
277
 
278
+ /**
279
+ * DOM-based extraction of hyperscript out of HTML markup. Shared with the
280
+ * testing-framework's shipped-sources validity gate so both harnesses agree on
281
+ * what counts as a live source (and neither regresses to a regex).
282
+ */
283
+ export { extractHyperscriptFromMarkup } from './html-snippets';
284
+ export type { MarkupSnippets } from './html-snippets';
285
+
278
286
  /**
279
287
  * Version of the package.
280
288
  */