@jasonbelmonti/markdown-engine 3.5.0 → 3.6.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 (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +10 -11
  3. package/dist/declarative-validation/assertions/id-targets.d.ts +2 -0
  4. package/dist/declarative-validation/assertions/id-targets.d.ts.map +1 -1
  5. package/dist/declarative-validation/assertions/id-targets.js.map +1 -1
  6. package/dist/declarative-validation/assertions/table-column-coverage.d.ts.map +1 -1
  7. package/dist/declarative-validation/assertions/table-column-coverage.js +8 -10
  8. package/dist/declarative-validation/assertions/table-column-coverage.js.map +1 -1
  9. package/dist/declarative-validation/assertions/text.d.ts.map +1 -1
  10. package/dist/declarative-validation/assertions/text.js +5 -0
  11. package/dist/declarative-validation/assertions/text.js.map +1 -1
  12. package/dist/declarative-validation/assertions/visible-text.d.ts +5 -0
  13. package/dist/declarative-validation/assertions/visible-text.d.ts.map +1 -0
  14. package/dist/declarative-validation/assertions/visible-text.js +42 -0
  15. package/dist/declarative-validation/assertions/visible-text.js.map +1 -0
  16. package/dist/declarative-validation/compiler/assertion-builders.js +11 -2
  17. package/dist/declarative-validation/compiler/assertion-builders.js.map +1 -1
  18. package/dist/declarative-validation/compiler/assertion-shapes.d.ts.map +1 -1
  19. package/dist/declarative-validation/compiler/assertion-shapes.js +20 -4
  20. package/dist/declarative-validation/compiler/assertion-shapes.js.map +1 -1
  21. package/dist/declarative-validation/compiler/plan.d.ts +4 -1
  22. package/dist/declarative-validation/compiler/plan.d.ts.map +1 -1
  23. package/dist/declarative-validation/profile/assertion-schema.d.ts.map +1 -1
  24. package/dist/declarative-validation/profile/assertion-schema.js +21 -6
  25. package/dist/declarative-validation/profile/assertion-schema.js.map +1 -1
  26. package/dist/declarative-validation/profile/cell-predicate-schema.d.ts +4 -0
  27. package/dist/declarative-validation/profile/cell-predicate-schema.d.ts.map +1 -0
  28. package/dist/declarative-validation/profile/cell-predicate-schema.js +29 -0
  29. package/dist/declarative-validation/profile/cell-predicate-schema.js.map +1 -0
  30. package/dist/declarative-validation/profile/index.d.ts +3 -0
  31. package/dist/declarative-validation/profile/index.d.ts.map +1 -1
  32. package/dist/declarative-validation/profile/selector-schema.d.ts.map +1 -1
  33. package/dist/declarative-validation/profile/selector-schema.js +1 -23
  34. package/dist/declarative-validation/profile/selector-schema.js.map +1 -1
  35. package/dist/declarative-validation/selectors/table-targets.d.ts +2 -1
  36. package/dist/declarative-validation/selectors/table-targets.d.ts.map +1 -1
  37. package/dist/declarative-validation/selectors/table-targets.js +13 -5
  38. package/dist/declarative-validation/selectors/table-targets.js.map +1 -1
  39. package/dist/internal/package-version.d.ts +1 -1
  40. package/dist/internal/package-version.js +1 -1
  41. package/dist-bundled/markdown-engine-cli.mjs +179 -67
  42. package/docs/contracts/declarative-validation.md +32 -1
  43. package/package.json +2 -2
  44. package/scripts/install-markdown-engine-cli.sh +2 -2
@@ -8047,7 +8047,8 @@ function tableColumnTargets(document3, source) {
8047
8047
  }
8048
8048
  const resolvedColumns = tableResolution.tables.flatMap((table) => {
8049
8049
  const columnIndex = columnIndexForHeader(table, source.column);
8050
- return columnIndex === void 0 ? [] : [{ table, columnIndex }];
8050
+ const predicateColumnMissing = source.rowWhere !== void 0 && columnIndexForHeader(table, source.rowWhere.column) === void 0;
8051
+ return columnIndex === void 0 || predicateColumnMissing ? [] : [{ table, columnIndex }];
8051
8052
  });
8052
8053
  if (resolvedColumns.length === 0) {
8053
8054
  return {
@@ -8060,7 +8061,7 @@ function tableColumnTargets(document3, source) {
8060
8061
  status: "resolved",
8061
8062
  source,
8062
8063
  targets: resolvedColumns.flatMap(
8063
- ({ table, columnIndex }) => tableCellTargetsForColumn(document3, table, columnIndex)
8064
+ ({ table, columnIndex }) => tableCellTargetsForColumn(document3, table, columnIndex, source.rowWhere)
8064
8065
  )
8065
8066
  };
8066
8067
  }
@@ -8110,9 +8111,11 @@ function rowMatchesPredicate(table, cells, predicate) {
8110
8111
  function columnIndexForHeader(table, column) {
8111
8112
  return headerCells(table).find((cell) => cell.text === column)?.columnIndex;
8112
8113
  }
8113
- function tableCellTargetsForColumn(document3, table, columnIndex) {
8114
- return dataRows(table).flatMap((row) => {
8115
- const cell = row.cells.find((candidate) => candidate.columnIndex === columnIndex);
8114
+ function tableCellTargetsForColumn(document3, table, columnIndex, rowWhere) {
8115
+ return dataRows(table).filter((row) => rowMatchesPredicate(table, row.cells, rowWhere)).flatMap((row) => {
8116
+ const cell = row.cells.find(
8117
+ (candidate) => candidate.columnIndex === columnIndex
8118
+ );
8116
8119
  return cell === void 0 ? [] : [tableCellSelectionTarget(document3, table, cell)];
8117
8120
  });
8118
8121
  }
@@ -9603,8 +9606,16 @@ function sourceLengthRequirement(assertion) {
9603
9606
  // src/declarative-validation/assertions/table-column-coverage.ts
9604
9607
  function evaluateTableColumnCoverage(assertion, context) {
9605
9608
  const options = idTokenOptions(assertion);
9606
- const sourceIds = sourceIdsForCoverage(assertion, context, options);
9609
+ const sourceResolution = resolveTableColumnIdTokens(
9610
+ context.selection.document,
9611
+ assertion.source,
9612
+ options
9613
+ );
9614
+ const sourceIds = sourceResolution.status === "resolved" ? uniqueSourceIds2(sourceResolution.tokens) : [];
9607
9615
  if (sourceIds.length === 0) {
9616
+ if (assertion.allowEmptySource === true && sourceResolution.status === "resolved") {
9617
+ return [];
9618
+ }
9608
9619
  return [emptySelectionDiagnostic(context.rule, context.assertionIndex)];
9609
9620
  }
9610
9621
  const targetResolution = resolveTableColumnIdTokens(
@@ -9614,12 +9625,22 @@ function evaluateTableColumnCoverage(assertion, context) {
9614
9625
  );
9615
9626
  if (targetResolution.status === "missingSection") {
9616
9627
  return sourceIds.map(
9617
- (sourceId, sourceIdOrder) => missingTargetSectionDiagnostic(assertion, context, sourceId, sourceIdOrder)
9628
+ (sourceId, sourceIdOrder) => missingTargetSectionDiagnostic(
9629
+ assertion,
9630
+ context,
9631
+ sourceId,
9632
+ sourceIdOrder
9633
+ )
9618
9634
  );
9619
9635
  }
9620
9636
  if (targetResolution.status === "missingColumn") {
9621
9637
  return sourceIds.map(
9622
- (sourceId, sourceIdOrder) => missingTargetColumnDiagnostic(assertion, context, sourceId, sourceIdOrder)
9638
+ (sourceId, sourceIdOrder) => missingTargetColumnDiagnostic(
9639
+ assertion,
9640
+ context,
9641
+ sourceId,
9642
+ sourceIdOrder
9643
+ )
9623
9644
  );
9624
9645
  }
9625
9646
  const targetComparisonValues = new Set(
@@ -9641,17 +9662,6 @@ function evaluateTableColumnCoverage(assertion, context) {
9641
9662
  }
9642
9663
  return diagnostics;
9643
9664
  }
9644
- function sourceIdsForCoverage(assertion, context, options) {
9645
- const sourceResolution = resolveTableColumnIdTokens(
9646
- context.selection.document,
9647
- {
9648
- section: assertion.source.section,
9649
- column: assertion.source.column
9650
- },
9651
- options
9652
- );
9653
- return sourceResolution.status === "resolved" ? uniqueSourceIds2(sourceResolution.tokens) : [];
9654
- }
9655
9665
  function uniqueSourceIds2(tokens) {
9656
9666
  const sourceIdsByComparisonValue = /* @__PURE__ */ new Map();
9657
9667
  for (const token of tokens) {
@@ -9824,6 +9834,40 @@ function tableColumnSortKey(table, diagnosticOrder, column) {
9824
9834
  return `table:${table.target.id}:column:${paddedDiagnosticOrder}:${column}`;
9825
9835
  }
9826
9836
 
9837
+ // src/declarative-validation/assertions/visible-text.ts
9838
+ function visibleText(document3, target) {
9839
+ const textAt = (nodeTarget) => documentQueries.nodes(document3, { targetId: nodeTarget.id }).map(nodeText2).join("");
9840
+ switch (target.kind) {
9841
+ case "document":
9842
+ return document3.children.map(nodeText2).join("\n");
9843
+ case "section":
9844
+ return [
9845
+ textAt(target.section.headingTarget),
9846
+ ...target.section.bodyTargets.map(textAt)
9847
+ ].join("\n");
9848
+ case "heading":
9849
+ return textAt(target.section.headingTarget);
9850
+ case "table":
9851
+ return textAt(target.table.target);
9852
+ case "tableRow":
9853
+ return target.cells.map((cell) => textAt(cell.target)).join(" ");
9854
+ case "tableCell":
9855
+ return textAt(target.cell.target);
9856
+ case "textSpan":
9857
+ return textAt(target.span.target);
9858
+ case "link":
9859
+ return textAt(target.link.target);
9860
+ case "list":
9861
+ return textAt(target.list.target);
9862
+ }
9863
+ }
9864
+ function nodeText2(node2) {
9865
+ if (node2.type === "html" || node2.type === "definition" || node2.type === "yaml") {
9866
+ return "";
9867
+ }
9868
+ return node2.children === void 0 ? node2.text ?? "" : node2.children.map(nodeText2).join("");
9869
+ }
9870
+
9827
9871
  // src/declarative-validation/assertions/literal-text.ts
9828
9872
  function countNonOverlappingLiteralOccurrences(text5, literal) {
9829
9873
  let count = 0;
@@ -9850,6 +9894,16 @@ function evaluateText(assertion, context) {
9850
9894
  }
9851
9895
  function evaluateTextTarget(assertion, context, target, targetOrder) {
9852
9896
  const diagnostics = [];
9897
+ if (assertion.nonBlank === true && visibleText(context.selection.document, target).trim().length === 0) {
9898
+ diagnostics.push(
9899
+ validationDiagnostic(
9900
+ "profile.validation.textBlank",
9901
+ `Selected ${target.kind} must contain non-whitespace text outside raw HTML.`,
9902
+ context.rule,
9903
+ { assertionIndex: context.assertionIndex, target, targetOrder }
9904
+ )
9905
+ );
9906
+ }
9853
9907
  if (assertion.contains !== void 0 && !target.text.includes(assertion.contains)) {
9854
9908
  diagnostics.push(
9855
9909
  validationDiagnostic(
@@ -10625,6 +10679,48 @@ function stringArray(value) {
10625
10679
  return values;
10626
10680
  }
10627
10681
 
10682
+ // src/declarative-validation/profile/cell-predicate-schema.ts
10683
+ function cellPredicateFromValue(value, key, diagnostics) {
10684
+ if (!isPlainRecord(value)) {
10685
+ diagnostics.push(invalidShape(`Selector ${key} must be an object.`));
10686
+ return void 0;
10687
+ }
10688
+ unsupportedKeys(value, ["column", "equals", "includes"], diagnostics);
10689
+ const column = requiredNonEmptyStringField(
10690
+ value.column,
10691
+ `Selector ${key}.column`,
10692
+ diagnostics,
10693
+ (field) => `${field} must be a non-empty string.`
10694
+ );
10695
+ if (column === void 0) {
10696
+ return void 0;
10697
+ }
10698
+ const predicate = {
10699
+ ...optionalPredicateString(value, "equals", diagnostics),
10700
+ ...optionalPredicateString(value, "includes", diagnostics)
10701
+ };
10702
+ if (predicate.equals === void 0 && predicate.includes === void 0) {
10703
+ diagnostics.push(
10704
+ invalidShape(
10705
+ `Selector ${key} must include at least one of equals or includes.`
10706
+ )
10707
+ );
10708
+ return void 0;
10709
+ }
10710
+ return {
10711
+ column,
10712
+ ...predicate
10713
+ };
10714
+ }
10715
+ function optionalPredicateString(record, key, diagnostics) {
10716
+ return optionalStringField(
10717
+ record,
10718
+ key,
10719
+ diagnostics,
10720
+ (field) => `Selector ${field} must be a non-empty string when provided.`
10721
+ );
10722
+ }
10723
+
10628
10724
  // src/declarative-validation/profile/selector-schema.ts
10629
10725
  function selectorFromValue(value, diagnostics) {
10630
10726
  if (!isPlainRecord(value)) {
@@ -10746,33 +10842,6 @@ function optionalCellPredicate(record, key, diagnostics) {
10746
10842
  const predicate = cellPredicateFromValue(value, key, diagnostics);
10747
10843
  return predicate === void 0 ? {} : { [key]: predicate };
10748
10844
  }
10749
- function cellPredicateFromValue(value, key, diagnostics) {
10750
- if (!isPlainRecord(value)) {
10751
- diagnostics.push(invalidShape(`Selector ${key} must be an object.`));
10752
- return void 0;
10753
- }
10754
- unsupportedKeys(value, ["column", "equals", "includes"], diagnostics);
10755
- const column = requiredSelectorString(value.column, `${key}.column`, diagnostics);
10756
- if (column === void 0) {
10757
- return void 0;
10758
- }
10759
- const predicate = {
10760
- ...optionalSelectorString(value, "equals", diagnostics),
10761
- ...optionalSelectorString(value, "includes", diagnostics)
10762
- };
10763
- if (predicate.equals === void 0 && predicate.includes === void 0) {
10764
- diagnostics.push(
10765
- invalidShape(
10766
- `Selector ${key} must include at least one of equals or includes.`
10767
- )
10768
- );
10769
- return void 0;
10770
- }
10771
- return {
10772
- column,
10773
- ...predicate
10774
- };
10775
- }
10776
10845
  function optionalBoolean(record, key, diagnostics) {
10777
10846
  return optionalBooleanField(
10778
10847
  record,
@@ -11122,7 +11191,22 @@ function pushObjectDiagnostic(fieldName, value, ruleId, diagnostics) {
11122
11191
  }
11123
11192
  function pushTextShapeDiagnostics(assertion, ruleId, diagnostics) {
11124
11193
  let valid = true;
11125
- if (assertion?.contains !== void 0 && !pushNonEmptyStringDiagnostic("contains", assertion.contains, ruleId, diagnostics)) {
11194
+ if (assertion?.nonBlank !== void 0 && assertion.nonBlank !== true) {
11195
+ diagnostics.push(
11196
+ compileDiagnostic(
11197
+ "profile.config.invalidShape",
11198
+ "text.nonBlank must be true when provided.",
11199
+ ruleId
11200
+ )
11201
+ );
11202
+ valid = false;
11203
+ }
11204
+ if (assertion?.contains !== void 0 && !pushNonEmptyStringDiagnostic(
11205
+ "contains",
11206
+ assertion.contains,
11207
+ ruleId,
11208
+ diagnostics
11209
+ )) {
11126
11210
  valid = false;
11127
11211
  }
11128
11212
  if (assertion?.excludes !== void 0 && closedStringArray("excludes", assertion.excludes, ruleId, diagnostics) === void 0) {
@@ -11191,7 +11275,13 @@ function closedTableColumnCoverage(value, ruleId, diagnostics) {
11191
11275
  const record = value;
11192
11276
  const hasSupportedKeys = pushUnsupportedKeyDiagnostics(
11193
11277
  record,
11194
- ["source", "target", "require"],
11278
+ ["source", "target", "require", "allowEmptySource"],
11279
+ diagnostics
11280
+ );
11281
+ pushOptionalBooleanDiagnostic(
11282
+ "tableColumnCoverage.allowEmptySource",
11283
+ record.allowEmptySource,
11284
+ ruleId,
11195
11285
  diagnostics
11196
11286
  );
11197
11287
  const source = closedTableColumnCoverageSource(
@@ -11217,7 +11307,8 @@ function closedTableColumnCoverage(value, ruleId, diagnostics) {
11217
11307
  return hasSupportedKeys && diagnostics.length === diagnosticCountBefore && source !== void 0 && target !== void 0 && require2 !== void 0 ? {
11218
11308
  source,
11219
11309
  target,
11220
- require: require2
11310
+ require: require2,
11311
+ ...record.allowEmptySource === void 0 ? {} : { allowEmptySource: record.allowEmptySource }
11221
11312
  } : void 0;
11222
11313
  }
11223
11314
  function closedTableColumnCoverageSource(value, ruleId, diagnostics) {
@@ -11232,7 +11323,7 @@ function closedTableColumnCoverageSource(value, ruleId, diagnostics) {
11232
11323
  const source = value;
11233
11324
  const hasSupportedKeys = pushUnsupportedKeyDiagnostics(
11234
11325
  source,
11235
- ["section", "column", "prefix", "caseSensitive"],
11326
+ ["section", "column", "prefix", "caseSensitive", "rowWhere"],
11236
11327
  diagnostics
11237
11328
  );
11238
11329
  const hasValidFields = pushNonEmptyStringDiagnostic(
@@ -11256,7 +11347,9 @@ function closedTableColumnCoverageSource(value, ruleId, diagnostics) {
11256
11347
  ruleId,
11257
11348
  diagnostics
11258
11349
  );
11259
- return hasSupportedKeys && hasValidFields ? {
11350
+ const rowWhere = source.rowWhere === void 0 ? void 0 : cellPredicateFromValue(source.rowWhere, "rowWhere", diagnostics);
11351
+ return hasSupportedKeys && hasValidFields && (source.rowWhere === void 0 || rowWhere !== void 0) ? {
11352
+ ...rowWhere === void 0 ? {} : { rowWhere },
11260
11353
  section: source.section,
11261
11354
  column: source.column,
11262
11355
  ...optionalString("prefix", source.prefix),
@@ -11431,7 +11524,7 @@ function optionalNumber2(key, value) {
11431
11524
  return value === void 0 ? {} : { [key]: value };
11432
11525
  }
11433
11526
  function hasTextPredicate(assertion) {
11434
- return assertion?.contains !== void 0 || assertion?.excludes !== void 0 && assertion.excludes.length > 0;
11527
+ return assertion?.nonBlank === true || assertion?.contains !== void 0 || assertion?.excludes !== void 0 && assertion.excludes.length > 0;
11435
11528
  }
11436
11529
 
11437
11530
  // src/declarative-validation/profile/ids-assertion-contract.ts
@@ -11856,7 +11949,9 @@ function buildTableColumnCoverageAssertion(assertion, selector, ruleId, syntaxVe
11856
11949
  )) {
11857
11950
  return {
11858
11951
  kind: "tableColumnCoverage",
11952
+ ...tableColumnCoverage.allowEmptySource === void 0 ? {} : { allowEmptySource: tableColumnCoverage.allowEmptySource },
11859
11953
  source: {
11954
+ ...tableColumnCoverage.source.rowWhere === void 0 ? {} : { rowWhere: tableColumnCoverage.source.rowWhere },
11860
11955
  section: tableColumnCoverage.source.section,
11861
11956
  column: tableColumnCoverage.source.column,
11862
11957
  caseSensitive: tableColumnCoverage.source.caseSensitive ?? true,
@@ -11895,13 +11990,13 @@ function buildFrontmatterShapeAssertion(assertion, selector, ruleId, syntaxVersi
11895
11990
  }
11896
11991
  return void 0;
11897
11992
  }
11898
- function buildTextAssertion(assertion, selector, ruleId, _syntaxVersion, diagnostics) {
11993
+ function buildTextAssertion(assertion, selector, ruleId, syntaxVersion, diagnostics) {
11899
11994
  if (assertion.text === void 0) {
11900
11995
  return void 0;
11901
11996
  }
11902
11997
  if (!pushObjectDiagnostic("text", assertion.text, ruleId, diagnostics) || !pushUnsupportedKeyDiagnostics(
11903
11998
  assertion.text,
11904
- ["contains", "excludes"],
11999
+ syntaxVersion === PROFILE_SYNTAX_VERSION_V2 ? ["contains", "excludes", "nonBlank"] : ["contains", "excludes"],
11905
12000
  diagnostics
11906
12001
  ) || !pushTextShapeDiagnostics(assertion.text, ruleId, diagnostics)) {
11907
12002
  return void 0;
@@ -11919,6 +12014,7 @@ function buildTextAssertion(assertion, selector, ruleId, _syntaxVersion, diagnos
11919
12014
  if (pushCompatibilityDiagnostic("text", selector, ruleId, diagnostics)) {
11920
12015
  return {
11921
12016
  kind: "text",
12017
+ ...assertion.text.nonBlank === true ? { nonBlank: true } : {},
11922
12018
  ...optionalString("contains", assertion.text.contains),
11923
12019
  ...optionalStringArray("excludes", assertion.text.excludes)
11924
12020
  };
@@ -12817,7 +12913,7 @@ function assertionFromValue(value, syntaxVersion, diagnostics) {
12817
12913
  ...tableColumnsRequiredFromValue(value.tableColumnsRequired, diagnostics),
12818
12914
  ...idsFromValue(value.ids, syntaxVersion, diagnostics),
12819
12915
  ...referencesFromValue(value.references, diagnostics),
12820
- ...textAssertionFromValue(value.text, diagnostics),
12916
+ ...textAssertionFromValue(value.text, syntaxVersion, diagnostics),
12821
12917
  ...textOccurrenceCountFromValue(value.textOccurrenceCount, diagnostics),
12822
12918
  ...textLengthFromValue(value.textLength, diagnostics),
12823
12919
  ...supportsV2AssertionSurface(syntaxVersion) ? selectionCountFromValue(value.selectionCount, diagnostics) : {},
@@ -12833,7 +12929,9 @@ function assertionFromValue(value, syntaxVersion, diagnostics) {
12833
12929
  return void 0;
12834
12930
  }
12835
12931
  diagnostics.push(
12836
- invalidShape("Rule assert must include at least one supported assertion.")
12932
+ invalidShape(
12933
+ "Rule assert must include at least one supported assertion."
12934
+ )
12837
12935
  );
12838
12936
  return void 0;
12839
12937
  }
@@ -13014,7 +13112,12 @@ function tableColumnCoverageFromValue(value, diagnostics) {
13014
13112
  diagnostics.push(invalidShape("tableColumnCoverage must be an object."));
13015
13113
  return {};
13016
13114
  }
13017
- unsupportedKeys(value, ["source", "target", "require"], diagnostics);
13115
+ unsupportedKeys(
13116
+ value,
13117
+ ["source", "target", "require", "allowEmptySource"],
13118
+ diagnostics
13119
+ );
13120
+ const emptyPolicy = optionalBoolean2(value, "allowEmptySource", diagnostics);
13018
13121
  const source = tableColumnCoverageSourceFromValue(value.source, diagnostics);
13019
13122
  const target = tableColumnCoverageTargetFromValue(value.target, diagnostics);
13020
13123
  const require2 = value.require === "everySourceId" ? "everySourceId" : void 0;
@@ -13025,6 +13128,7 @@ function tableColumnCoverageFromValue(value, diagnostics) {
13025
13128
  }
13026
13129
  return source === void 0 || target === void 0 || require2 === void 0 ? {} : {
13027
13130
  tableColumnCoverage: {
13131
+ ...emptyPolicy,
13028
13132
  source,
13029
13133
  target,
13030
13134
  require: require2
@@ -13041,7 +13145,7 @@ function tableColumnCoverageSourceFromValue(value, diagnostics) {
13041
13145
  }
13042
13146
  unsupportedKeys(
13043
13147
  value,
13044
- ["section", "column", "prefix", "caseSensitive"],
13148
+ ["section", "column", "prefix", "caseSensitive", "rowWhere"],
13045
13149
  diagnostics
13046
13150
  );
13047
13151
  const section = requiredAssertionString(
@@ -13054,7 +13158,9 @@ function tableColumnCoverageSourceFromValue(value, diagnostics) {
13054
13158
  "tableColumnCoverage.source.column",
13055
13159
  diagnostics
13056
13160
  );
13161
+ const rowWhere = value.rowWhere === void 0 ? void 0 : cellPredicateFromValue(value.rowWhere, "rowWhere", diagnostics);
13057
13162
  const source = {
13163
+ ...rowWhere === void 0 ? {} : { rowWhere },
13058
13164
  ...optionalTableColumnCoverageSourceString(value, "prefix", diagnostics),
13059
13165
  ...optionalTableColumnCoverageSourceBoolean(
13060
13166
  value,
@@ -13137,7 +13243,7 @@ function frontmatterShapeFromValue2(value, diagnostics) {
13137
13243
  }
13138
13244
  return result.shape === void 0 ? {} : { frontmatterShape: result.shape };
13139
13245
  }
13140
- function textAssertionFromValue(value, diagnostics) {
13246
+ function textAssertionFromValue(value, syntaxVersion, diagnostics) {
13141
13247
  if (value === void 0) {
13142
13248
  return {};
13143
13249
  }
@@ -13145,16 +13251,22 @@ function textAssertionFromValue(value, diagnostics) {
13145
13251
  diagnostics.push(invalidShape("text must be an object."));
13146
13252
  return {};
13147
13253
  }
13148
- unsupportedKeys(value, ["contains", "excludes"], diagnostics);
13254
+ unsupportedKeys(
13255
+ value,
13256
+ syntaxVersion === PROFILE_SYNTAX_VERSION_V2 ? ["contains", "excludes", "nonBlank"] : ["contains", "excludes"],
13257
+ diagnostics
13258
+ );
13259
+ if (value.nonBlank !== void 0 && value.nonBlank !== true) {
13260
+ diagnostics.push(invalidShape("text.nonBlank must be true when provided."));
13261
+ }
13149
13262
  const text5 = {
13263
+ ...value.nonBlank === true ? { nonBlank: true } : {},
13150
13264
  ...optionalAssertionString(value, "contains", diagnostics),
13151
13265
  ...optionalStringArray2(value, "excludes", diagnostics)
13152
13266
  };
13153
13267
  if (!hasTextPredicate2(text5)) {
13154
13268
  diagnostics.push(
13155
- invalidShape(
13156
- "text must include contains or a non-empty excludes array."
13157
- )
13269
+ invalidShape("text must include contains or a non-empty excludes array.")
13158
13270
  );
13159
13271
  return {};
13160
13272
  }
@@ -13299,7 +13411,7 @@ function requiredAssertionString(value, field, diagnostics) {
13299
13411
  );
13300
13412
  }
13301
13413
  function hasTextPredicate2(text5) {
13302
- return text5?.contains !== void 0 || text5?.excludes !== void 0;
13414
+ return text5?.nonBlank === true || text5?.contains !== void 0 || text5?.excludes !== void 0;
13303
13415
  }
13304
13416
  function isNonNegativeInteger2(value) {
13305
13417
  return Number.isInteger(value) && value >= 0;
@@ -13814,7 +13926,7 @@ function cloneSourceRange2(sourceRange) {
13814
13926
  import { createHash } from "node:crypto";
13815
13927
 
13816
13928
  // src/internal/package-version.ts
13817
- var MARKDOWN_ENGINE_PACKAGE_VERSION = "3.5.0";
13929
+ var MARKDOWN_ENGINE_PACKAGE_VERSION = "3.6.0";
13818
13930
 
13819
13931
  // src/internal/stable-json.ts
13820
13932
  function normalizeStableJsonValue(value) {
@@ -26608,7 +26720,7 @@ function toEngineNode(node2, options) {
26608
26720
  const type = typeof node2.type === "string" ? node2.type : "unknown";
26609
26721
  const attributes = nodeAttributes(type, node2, options);
26610
26722
  const sourceRange = toSourceRange(node2.position, options);
26611
- const text5 = nodeText2(type, node2);
26723
+ const text5 = nodeText3(type, node2);
26612
26724
  const children = childNodes(node2).map((child) => toEngineNode(child, options));
26613
26725
  return {
26614
26726
  type,
@@ -26715,7 +26827,7 @@ function assignStringAttribute(attributes, name, value) {
26715
26827
  function isTableAlignment(alignment) {
26716
26828
  return alignment === "left" || alignment === "right" || alignment === "center" || alignment === null;
26717
26829
  }
26718
- function nodeText2(type, node2) {
26830
+ function nodeText3(type, node2) {
26719
26831
  if (typeof node2.value === "string") {
26720
26832
  return node2.value;
26721
26833
  }
@@ -323,7 +323,9 @@ interface DeclarativeAssertion {
323
323
  mustAppearIn: readonly string[];
324
324
  };
325
325
  tableColumnCoverage?: {
326
+ allowEmptySource?: boolean;
326
327
  source: {
328
+ rowWhere?: { column: string; equals?: string; includes?: string };
327
329
  section: string;
328
330
  column: string;
329
331
  prefix?: string;
@@ -349,6 +351,7 @@ interface DeclarativeAssertion {
349
351
  }[];
350
352
  };
351
353
  text?: {
354
+ nonBlank?: true; // v2 only
352
355
  contains?: string;
353
356
  excludes?: readonly string[];
354
357
  };
@@ -461,6 +464,22 @@ target section do not satisfy coverage. Missing target sections, missing target
461
464
  columns, and missing target-column IDs emit deterministic validation diagnostics
462
465
  source-grounded to the source ID when source evidence is available.
463
466
 
467
+ The opt-in `source.rowWhere` filter uses the same closed sibling-cell predicate
468
+ as the `tableCell` selector: a non-empty `column` plus `equals`, `includes`, or
469
+ both. Both predicates must match when both are present. Matching uses normalized,
470
+ case-sensitive cell text before ID extraction; `source.caseSensitive` affects ID
471
+ comparison only. Rows marked optional can therefore be excluded without encoding
472
+ consumer vocabulary in the engine. A missing predicate column is unresolved,
473
+ not a resolved empty source.
474
+
475
+ `allowEmptySource` is an optional boolean. Absent or `false` preserves the existing
476
+ `profile.validation.emptySelection` failure when no source IDs are extracted.
477
+ `true` permits zero extracted IDs only when the source section and column resolve
478
+ (including the predicate column when configured). With no source IDs there are no
479
+ target obligations; target resolution is not evaluated. A missing source structure
480
+ still fails. Pair this policy with consumer shape rules when tables or rows are
481
+ mandatory. This option also permits prose sentinels in reverse declared-ID checks.
482
+
464
483
  For v2 profiles, `frontmatterShape` is admitted as a flat-rule schema and
465
484
  internal compiled-plan assertion. It is compatible only with a `document`
466
485
  selector because frontmatter is document metadata. `presence` is optional and
@@ -505,7 +524,18 @@ is combined with `equals` or `nonBlank`, a non-string value emits only
505
524
  string-predicate diagnostic. Diagnostic messages do not include frontmatter
506
525
  values.
507
526
 
508
- `text` must include `contains` or a non-empty `excludes` array.
527
+ `text` must include `contains`, a non-empty `excludes` array, or (v2 only)
528
+ `nonBlank: true`. `nonBlank` cannot be `false`. It requires at least one character
529
+ after JavaScript `trim()` on normalized node text excluding raw `html`, `definition`,
530
+ and `yaml` nodes. Comments, empty HTML tags, and decoded whitespace entities cannot
531
+ satisfy it. Markdown emphasis, link labels, literal code, and escaped angle brackets
532
+ retain their text. Each selected target is checked independently; an empty selector
533
+ still fails. Section selection includes its heading and direct body, as with ordinary
534
+ section text. Original source ranges are optional; the normalized nodes provide the
535
+ content. This is a structural textual-content check, not CSS visibility evaluation
536
+ or browser rendering. It does not evaluate external evidence, freshness, truth,
537
+ or implementation acceptance. Existing `contains`, `excludes`, and `textLength`
538
+ semantics are unchanged, even when combined with `nonBlank`.
509
539
  `textOccurrenceCount.count` is a finite number and counts non-overlapping
510
540
  literal occurrences per selected target.
511
541
  `textLength` must include `min`, `max`, or both. Bounds are non-negative
@@ -592,6 +622,7 @@ an unproven pass.
592
622
  | `profile.validation.tableColumnCoverageIdMissing` | Rule severity | A source ID is absent from the configured target table column. |
593
623
  | `profile.validation.tableColumnCoverageTargetColumnMissing` | Rule severity | The configured target table column cannot be resolved. |
594
624
  | `profile.validation.tableColumnCoverageTargetSectionMissing` | Rule severity | The configured target section cannot be resolved. |
625
+ | `profile.validation.textBlank` | Rule severity | A target configured with `text.nonBlank` lacks non-whitespace text outside raw HTML. |
595
626
  | `profile.validation.textExcluded` | Rule severity | A selected target contains forbidden literal text. |
596
627
  | `profile.validation.textMissing` | Rule severity | A selected target lacks required literal text from a `text.contains` assertion. |
597
628
  | `profile.validation.assertionUnsupported` | `error` | A compiled assertion or compiled assertion feature has no evaluator implementation; this is an internal safety diagnostic. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonbelmonti/markdown-engine",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Deterministic Markdown parsing and validation engine package.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -71,7 +71,7 @@
71
71
  "test:validation:profile": "npm run build && vitest run tests/declarative-validation-profile.test.ts \"--exclude=.worktrees/**\"",
72
72
  "test:validation:compiler": "npm run build && vitest run tests/declarative-validation-compiler.test.ts \"--exclude=.worktrees/**\"",
73
73
  "test:validation:selectors": "npm run build && vitest run tests/declarative-validation-selectors.test.ts \"--exclude=.worktrees/**\"",
74
- "test:validation:assertions": "npm run build && vitest run tests/declarative-validation-assertions.test.ts tests/declarative-validation-frontmatter-shape-assertions.test.ts tests/declarative-validation-table-column-coverage-fixtures.test.ts tests/declarative-validation-table-columns-exact.test.ts tests/declarative-validation-source-assertion-coverage.test.ts tests/declarative-validation-source-length.test.ts tests/declarative-validation-selection-count.test.ts tests/declarative-validation-normalized-source-ranges.test.ts tests/declarative-validation-grouped-rules-fixtures.test.ts tests/declarative-validation-when-skipped-rules-fixtures.test.ts \"--exclude=.worktrees/**\"",
74
+ "test:validation:assertions": "npm run build && vitest run tests/declarative-validation-assertions.test.ts tests/declarative-validation-filtered-coverage.test.ts tests/declarative-validation-visible-text.test.ts tests/declarative-validation-frontmatter-shape-assertions.test.ts tests/declarative-validation-table-column-coverage-fixtures.test.ts tests/declarative-validation-table-columns-exact.test.ts tests/declarative-validation-source-assertion-coverage.test.ts tests/declarative-validation-source-length.test.ts tests/declarative-validation-selection-count.test.ts tests/declarative-validation-normalized-source-ranges.test.ts tests/declarative-validation-grouped-rules-fixtures.test.ts tests/declarative-validation-when-skipped-rules-fixtures.test.ts \"--exclude=.worktrees/**\"",
75
75
  "test:validation:diagnostics": "npm run build && vitest run tests/declarative-validation-diagnostics.test.ts \"--exclude=.worktrees/**\"",
76
76
  "test:validation:cli": "npm run build && vitest run tests/declarative-validation-cli.test.ts \"--exclude=.worktrees/**\"",
77
77
  "test:validation:examples": "npm run build && vitest run tests/declarative-validation-examples.test.ts \"--exclude=.worktrees/**\"",
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env sh
2
2
  set -eu
3
3
 
4
- VERSION="3.5.0"
4
+ VERSION="3.6.0"
5
5
  PACKAGE="@jasonbelmonti/markdown-engine"
6
- EXPECTED_SHA256="4a4e9ae7d9ff4797c96a7483555bcba7cbec5852d330c6c50dc0ef8fb2e5d933"
6
+ EXPECTED_SHA256="69b855d4d3623d7cdb91a3ed09631715dde8b96fc57f49864ae2c7c7c071ad83"
7
7
  DEFAULT_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
8
8
  MARKDOWN_ENGINE_HOME="${MARKDOWN_ENGINE_HOME:-$DEFAULT_DATA_HOME/markdown-engine}"
9
9
  MARKDOWN_ENGINE_BIN_DIR="${MARKDOWN_ENGINE_BIN_DIR:-$HOME/.local/bin}"