@openpresentation/opf 0.26.5 → 0.26.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +13 -13
- package/dist/{chunk-UY6SQVR2.js → chunk-3S2XVDPN.js} +5 -5
- package/dist/{chunk-L7YEVQHP.js → chunk-4K4G3JXR.js} +6 -6
- package/dist/{chunk-EFMR32A5.js → chunk-77KL2PMM.js} +1 -1
- package/dist/{chunk-TAV5S2JG.js → chunk-A54NGT2K.js} +1 -1
- package/dist/{chunk-UFEWQDQI.js → chunk-D25BNPTW.js} +1 -1
- package/dist/{chunk-MD3CJ5YJ.js → chunk-G5WJMJV3.js} +1 -1
- package/dist/{chunk-STIYINEL.js → chunk-GIWNHRTX.js} +3 -3
- package/dist/{chunk-MLH5LIGF.js → chunk-H7XMO36O.js} +4 -4
- package/dist/{chunk-CIWC7DRZ.js → chunk-IVPA6NG6.js} +514 -481
- package/dist/{chunk-K5Q3LLYK.js → chunk-NVT2YDVC.js} +2 -2
- package/dist/{chunk-DXTPLVAK.js → chunk-PHP45MO7.js} +1 -1
- package/dist/{chunk-WAZDP2WS.js → chunk-VZBFQLCA.js} +3 -3
- package/dist/{chunk-HPEZCAYC.js → chunk-Y25DYRME.js} +1 -1
- package/dist/{chunk-KPALXWX3.js → chunk-Y4TZCE7R.js} +2 -2
- package/dist/{conversion-C7o2EpDb.d.ts → conversion-16M7eOGP.d.ts} +2 -2
- package/dist/conversion-F3T2FUP3.js +21 -0
- package/dist/convert.js +5 -5
- package/dist/deck.js +8 -8
- package/dist/diff.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -17
- package/dist/markdown.js +6 -6
- package/dist/node-engine.d.ts +2 -2
- package/dist/node-engine.js +17 -17
- package/dist/pagination.js +2 -2
- package/dist/patch.js +1 -1
- package/dist/schema.js +1 -1
- package/dist/source.js +8 -8
- package/dist/validator.d.ts +8 -3
- package/dist/validator.js +1 -1
- package/dist/yaml.js +4 -4
- package/package.json +5 -5
- package/dist/conversion-PMJWLHDV.js +0 -21
|
@@ -1158,9 +1158,6 @@ function describeDurationRange(range) {
|
|
|
1158
1158
|
return range.min !== void 0 ? `at least ${range.min} minutes` : `at most ${range.max} minutes`;
|
|
1159
1159
|
}
|
|
1160
1160
|
|
|
1161
|
-
// src/rule-context.ts
|
|
1162
|
-
var rule = (id, category, severity, summary, rationale, extra = {}) => ({ id: `opf/${id}`, name: id, category, severity, cost: extra.cost ?? "structure", summary, rationale, ...extra });
|
|
1163
|
-
|
|
1164
1161
|
// src/rule-design.ts
|
|
1165
1162
|
var rec = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1166
1163
|
var toRgb = (hex) => {
|
|
@@ -1580,7 +1577,502 @@ function assetRefs(document, slide, slidePath, includeDeck) {
|
|
|
1580
1577
|
out.push({ path: Array.isArray(document.speaker) ? `/speaker/${index}/photo` : "/speaker/photo", value: speaker.photo, kind: "speaker" });
|
|
1581
1578
|
});
|
|
1582
1579
|
}
|
|
1583
|
-
return out;
|
|
1580
|
+
return out;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/rule-info.ts
|
|
1584
|
+
var rule = (id, category, severity, summary, rationale, extra = {}) => ({ id: `opf/${id}`, name: id, category, severity, cost: extra.cost ?? "structure", summary, rationale, ...extra });
|
|
1585
|
+
|
|
1586
|
+
// src/validation-types.ts
|
|
1587
|
+
var validationCategories = Object.freeze([
|
|
1588
|
+
"format",
|
|
1589
|
+
"references",
|
|
1590
|
+
"policy",
|
|
1591
|
+
"accessibility",
|
|
1592
|
+
"layout",
|
|
1593
|
+
"content"
|
|
1594
|
+
]);
|
|
1595
|
+
|
|
1596
|
+
// src/validation-rule-info.ts
|
|
1597
|
+
var DEFAULT_VALIDATION_THRESHOLDS = Object.freeze({
|
|
1598
|
+
contrastNormal: 4.5,
|
|
1599
|
+
contrastLarge: 3,
|
|
1600
|
+
minFontSizePt: 11,
|
|
1601
|
+
minImagePpi: 96,
|
|
1602
|
+
minSeriesColorDifference: 10
|
|
1603
|
+
});
|
|
1604
|
+
var SCHEMA = "spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF";
|
|
1605
|
+
var formatRules = [
|
|
1606
|
+
rule(
|
|
1607
|
+
"json-syntax",
|
|
1608
|
+
"format",
|
|
1609
|
+
"error",
|
|
1610
|
+
"The text is not strict JSON.",
|
|
1611
|
+
"Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.",
|
|
1612
|
+
{ cost: "syntax", standard: "RFC 8259 (strict JSON: no comments, no trailing commas)", approximations: "Only text input is checked. A string is always read as JSON. One finding per root cause, in position order and at most five (a key with no value, a missing comma, a bracket never closed; not the damage each one causes further on): `message` says what is wrong, `help` what to do next and `location` is the range to underline. A text that looks like YAML or Markdown gets one finding with a hint to use fromYaml or fromMarkdown first. Source editors get the same causes with their quick fixes from analyzeJsonSyntax (@openpresentation/opf/source)." }
|
|
1613
|
+
),
|
|
1614
|
+
rule(
|
|
1615
|
+
"duplicate-key",
|
|
1616
|
+
"format",
|
|
1617
|
+
"error",
|
|
1618
|
+
"A JSON object repeats a key.",
|
|
1619
|
+
"JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.",
|
|
1620
|
+
{ cost: "syntax", standard: "RFC 8259 section 4 (names within an object should be unique)" }
|
|
1621
|
+
),
|
|
1622
|
+
rule(
|
|
1623
|
+
"schema",
|
|
1624
|
+
"format",
|
|
1625
|
+
"error",
|
|
1626
|
+
"The document breaks the OPF schema or a structural rule the schema cannot express.",
|
|
1627
|
+
"The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.",
|
|
1628
|
+
{ standard: SCHEMA, approximations: "All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category." }
|
|
1629
|
+
),
|
|
1630
|
+
rule("reference-id-duplicate", "format", "error", "Two references share an id.", "A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.", { standard: SCHEMA }),
|
|
1631
|
+
rule("cite-unknown-reference", "format", "error", "A citation names a reference that does not exist.", "A cited id with no entry in `references` has nothing to show in the footnote area.", { standard: SCHEMA }),
|
|
1632
|
+
rule("cite-unsupported-location", "format", "error", "A citation or footnote sits where no engine draws it.", "`cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.", { standard: SCHEMA }),
|
|
1633
|
+
rule("caption-unsupported-payload", "format", "error", "A caption is on a payload that cannot carry one.", "A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.", { standard: SCHEMA }),
|
|
1634
|
+
rule("image-option-unsupported-payload", "format", "error", "An image option is on a payload that is not an image.", "`fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.", { standard: SCHEMA }),
|
|
1635
|
+
rule("image-placement-invalid", "format", "error", "An image placement cannot be honoured.", "A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.", { standard: SCHEMA }),
|
|
1636
|
+
rule("layout-placeholder-group", "format", "error", "A layout record's placeholder group breaks a nesting rule.", "A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.", { standard: SCHEMA }),
|
|
1637
|
+
rule("dataset-unknown", "format", "error", "A chart or table names a dataset that does not exist.", "The data cannot be drawn without the dataset it refers to.", { standard: SCHEMA }),
|
|
1638
|
+
rule("dataset-field-unknown", "format", "error", "A table asks a dataset for a column it does not have.", "The column cannot be shown when the dataset has no column of that name.", { standard: SCHEMA }),
|
|
1639
|
+
rule("data-column-duplicate", "format", "error", "A dataset or table has two columns with the same name.", "Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.", { standard: SCHEMA }),
|
|
1640
|
+
rule("chart-mapping-unknown-column", "format", "error", "A chart mapping names a column the data does not have.", "The series cannot be built from a column that is not there.", { standard: SCHEMA }),
|
|
1641
|
+
rule("number-format-invalid", "format", "error", "A number format cannot be read.", "A format code that no engine can parse is drawn as the plain number or fails to export.", { standard: "Excel number format codes, as far as OPF supports them" }),
|
|
1642
|
+
rule("variable-invalid-value", "format", "error", "A variable value does not match its declared type.", "A number variable given text, or a date given a non-date, cannot be filled into the deck.", { standard: SCHEMA }),
|
|
1643
|
+
rule("variable-unknown-builtin", "format", "error", "A token names a built-in variable that does not exist.", "The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.", { standard: SCHEMA }),
|
|
1644
|
+
rule("chart-highlight-unknown-name", "format", "error", "A chart highlight names a series or category the chart does not have.", "`chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.", { standard: SCHEMA }),
|
|
1645
|
+
rule("variable-format", "format", "error", "A variable cannot be formatted.", "A number or date format that does not apply to the value leaves the token unreadable.", { standard: SCHEMA }),
|
|
1646
|
+
rule(
|
|
1647
|
+
"variable-unfilled",
|
|
1648
|
+
"format",
|
|
1649
|
+
"error",
|
|
1650
|
+
"A required template variable has no value.",
|
|
1651
|
+
"A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.",
|
|
1652
|
+
{
|
|
1653
|
+
approximations: "Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings."
|
|
1654
|
+
}
|
|
1655
|
+
),
|
|
1656
|
+
rule("run-color-unrecognized", "format", "warning", "A text run colour is none of the documented forms.", "Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.", { standard: SCHEMA }),
|
|
1657
|
+
rule("numbering-start-ignored", "format", "warning", "A list entry sets `start` where nothing is numbered.", "A start value only restarts an auto-number; without `numbering` on the payload it has no effect.", { standard: SCHEMA }),
|
|
1658
|
+
rule(
|
|
1659
|
+
"slide-size-out-of-range",
|
|
1660
|
+
"format",
|
|
1661
|
+
"warning",
|
|
1662
|
+
"The slide size is outside PowerPoint's 1 to 56 inch range.",
|
|
1663
|
+
"The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.",
|
|
1664
|
+
{
|
|
1665
|
+
standard: "ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU",
|
|
1666
|
+
approximations: "Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured."
|
|
1667
|
+
}
|
|
1668
|
+
)
|
|
1669
|
+
];
|
|
1670
|
+
var referenceRules = [
|
|
1671
|
+
rule(
|
|
1672
|
+
"asset-reference",
|
|
1673
|
+
"references",
|
|
1674
|
+
"error",
|
|
1675
|
+
"An `asset:` reference names an asset that is not in the registry.",
|
|
1676
|
+
"An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.",
|
|
1677
|
+
{ standard: SCHEMA }
|
|
1678
|
+
),
|
|
1679
|
+
rule("asset-cycle", "references", "error", "Asset references form a cycle.", "An asset whose source points back at itself never resolves to a file.", { standard: SCHEMA }),
|
|
1680
|
+
rule(
|
|
1681
|
+
"catalog-record",
|
|
1682
|
+
"references",
|
|
1683
|
+
"error",
|
|
1684
|
+
"An embedded or registered catalog record is invalid.",
|
|
1685
|
+
"A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.",
|
|
1686
|
+
{ standard: "spec/schemas/<kind>.schema.json" }
|
|
1687
|
+
),
|
|
1688
|
+
rule(
|
|
1689
|
+
"layout-template",
|
|
1690
|
+
"references",
|
|
1691
|
+
"error",
|
|
1692
|
+
"A layout template's region list is malformed.",
|
|
1693
|
+
`A layout is one ordered list of regions, nested through splits for two dimensions. Engines cannot compose a list that is not 1 to 12 entries, an entry that is neither a region nor a split, a direction or size that is not valid, an "auto" size anywhere but in a column, splits nested more than three levels deep (the record's own list is level 1), or regions that compile to a grid larger than 12 x 12. composeSlide refuses such a record, so a slide that names it composes automatically.`,
|
|
1694
|
+
{ standard: "spec/schemas/layout.schema.json and the region list rules (docs/layout-templates.md)", approximations: "Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema." }
|
|
1695
|
+
),
|
|
1696
|
+
rule(
|
|
1697
|
+
"layout-region",
|
|
1698
|
+
"references",
|
|
1699
|
+
"error",
|
|
1700
|
+
"A layout template's regions are not valid.",
|
|
1701
|
+
"Region names are unique across the whole record and may not be tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; title and subtitle name the heading regions, which take only a size; a subtitle region needs a title region; a region accepts at least one content kind; a region with flow 'none' holds one block; overflowRegion must name a body region. composeSlide refuses such a record.",
|
|
1702
|
+
{ standard: "spec/schemas/layout.schema.json and the region rules (docs/layout-templates.md)" }
|
|
1703
|
+
),
|
|
1704
|
+
rule(
|
|
1705
|
+
"unresolved-reference",
|
|
1706
|
+
"references",
|
|
1707
|
+
"warning",
|
|
1708
|
+
"A content reference resolves nowhere.",
|
|
1709
|
+
"A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.",
|
|
1710
|
+
{ standard: SCHEMA, approximations: "Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference." }
|
|
1711
|
+
),
|
|
1712
|
+
rule(
|
|
1713
|
+
"catalog-record-not-in-source",
|
|
1714
|
+
"references",
|
|
1715
|
+
"warning",
|
|
1716
|
+
"A record embedded under a catalog group is not in that catalog.",
|
|
1717
|
+
"A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.",
|
|
1718
|
+
{ standard: SCHEMA, approximations: "Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked." }
|
|
1719
|
+
),
|
|
1720
|
+
rule("undeclared-catalog", "format", "error", "A reference names a catalog group the document does not declare.", "The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.", { standard: SCHEMA }),
|
|
1721
|
+
rule("unused-reference", "references", "warning", "A reference is never cited.", "A listed reference that no run cites is not drawn anywhere.", { standard: SCHEMA }),
|
|
1722
|
+
rule("unused-dataset", "references", "warning", "A dataset is never used.", "A dataset no chart or table references is not drawn anywhere.", { standard: SCHEMA }),
|
|
1723
|
+
rule("unknown-beat", "references", "warning", "A slide names a beat its narrative does not define.", "`slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.", { standard: SCHEMA, approximations: "The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked." }),
|
|
1724
|
+
rule("slide-theme-dimensions", "references", "warning", "A slide theme sets a slide size that differs from the deck.", "A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.", { standard: SCHEMA }),
|
|
1725
|
+
rule("variable-unknown", "references", "warning", "A `{{token}}` names a variable that is not declared.", "An undeclared token is shown literally.", { standard: SCHEMA }),
|
|
1726
|
+
rule("variable-unknown-value", "references", "warning", "A value was supplied for a variable that is not declared.", "The value is never used.", { standard: SCHEMA }),
|
|
1727
|
+
rule("variable-unused", "references", "warning", "A variable is declared but never used.", "A declaration nothing refers to has no effect.", { standard: SCHEMA }),
|
|
1728
|
+
rule("variable-reference-unknown", "references", "warning", "A `var:` colour names a variable that is not declared.", "An undeclared colour variable falls back to a default colour.", { standard: SCHEMA })
|
|
1729
|
+
];
|
|
1730
|
+
var policyRules = [
|
|
1731
|
+
rule(
|
|
1732
|
+
"contract",
|
|
1733
|
+
"policy",
|
|
1734
|
+
"error",
|
|
1735
|
+
"A field is outside the values a host policy allows.",
|
|
1736
|
+
"A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.",
|
|
1737
|
+
{ approximations: "Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default." }
|
|
1738
|
+
)
|
|
1739
|
+
];
|
|
1740
|
+
var dataRules = [
|
|
1741
|
+
rule("chart-option-adapted", "layout", "warning", "A chart option cannot be shown by the chart type.", "An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.", { standard: SCHEMA }),
|
|
1742
|
+
rule(
|
|
1743
|
+
"chart-value-not-numeric",
|
|
1744
|
+
"content",
|
|
1745
|
+
"warning",
|
|
1746
|
+
"A chart value is text, not a number.",
|
|
1747
|
+
'A chart plots numbers. A text cell such as "12%" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.',
|
|
1748
|
+
{ standard: SCHEMA }
|
|
1749
|
+
),
|
|
1750
|
+
rule("chart-mapping-adapted", "content", "warning", "A chart mapping entry is ignored.", "A mapping that names a column the chart type cannot use is left out of the chart.", { standard: SCHEMA }),
|
|
1751
|
+
rule("chart-highlight-adapted", "content", "warning", "A chart highlight is adapted by the chart type.", "A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.", { standard: SCHEMA }),
|
|
1752
|
+
rule("code-highlight-out-of-range", "content", "warning", "A code highlight marks a line the code does not have.", "Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.", { standard: SCHEMA }),
|
|
1753
|
+
rule("code-highlight-range-reversed", "content", "warning", "A code highlight range ends before it starts.", "A range written end before start marks nothing; write the smaller line number first.", { standard: SCHEMA }),
|
|
1754
|
+
rule("variable-builtin-missing", "content", "warning", "A built-in variable has no value in this document.", "A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.", { standard: SCHEMA }),
|
|
1755
|
+
rule("duration-outside-narrative", "content", "warning", "The target duration is outside the range the narrative suits.", "A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.", { standard: SCHEMA }),
|
|
1756
|
+
rule("narrative-duration-range", "content", "warning", "An inline narrative duration range has min greater than max.", "A range with its bounds swapped matches no duration, so the duration check cannot help.", { standard: "spec/schemas/narrative.schema.json" })
|
|
1757
|
+
];
|
|
1758
|
+
var contrastRule = rule(
|
|
1759
|
+
"text-contrast",
|
|
1760
|
+
"accessibility",
|
|
1761
|
+
"warning",
|
|
1762
|
+
"Text colour has too little contrast against the background it sits on.",
|
|
1763
|
+
"Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.",
|
|
1764
|
+
{
|
|
1765
|
+
standard: "WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA",
|
|
1766
|
+
thresholds: ["contrastNormal", "contrastLarge"],
|
|
1767
|
+
cost: "composition",
|
|
1768
|
+
approximations: "Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour."
|
|
1769
|
+
}
|
|
1770
|
+
);
|
|
1771
|
+
var onImageRule = rule(
|
|
1772
|
+
"text-on-image",
|
|
1773
|
+
"accessibility",
|
|
1774
|
+
"info",
|
|
1775
|
+
"Text sits on a background picture whose pixels cannot be measured.",
|
|
1776
|
+
"Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.",
|
|
1777
|
+
{
|
|
1778
|
+
standard: "WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA",
|
|
1779
|
+
thresholds: ["contrastNormal", "contrastLarge"],
|
|
1780
|
+
cost: "composition",
|
|
1781
|
+
approximations: "Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted."
|
|
1782
|
+
}
|
|
1783
|
+
);
|
|
1784
|
+
var altRule = rule(
|
|
1785
|
+
"missing-alt-text",
|
|
1786
|
+
"accessibility",
|
|
1787
|
+
"warning",
|
|
1788
|
+
"A picture has no alt text and is not marked decorative.",
|
|
1789
|
+
'People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: ""), which is an explicit, reviewed choice instead of an omission.',
|
|
1790
|
+
{
|
|
1791
|
+
standard: "WCAG 2.2 SC 1.1.1 Non-text Content, level A",
|
|
1792
|
+
approximations: "Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked."
|
|
1793
|
+
}
|
|
1794
|
+
);
|
|
1795
|
+
var poorAltRule = rule(
|
|
1796
|
+
"poor-alt-text",
|
|
1797
|
+
"accessibility",
|
|
1798
|
+
"info",
|
|
1799
|
+
"Alt text is a file name, a URL, a generic word or very long.",
|
|
1800
|
+
'Alt text such as "image", "IMG_2041.png" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.',
|
|
1801
|
+
{ standard: "WCAG 2.2 SC 1.1.1 Non-text Content, level A", approximations: 'Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading "image of", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading "chart of" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.' }
|
|
1802
|
+
);
|
|
1803
|
+
var missingTitleRule = rule(
|
|
1804
|
+
"missing-slide-title",
|
|
1805
|
+
"accessibility",
|
|
1806
|
+
"warning",
|
|
1807
|
+
"A slide has no title.",
|
|
1808
|
+
"Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.",
|
|
1809
|
+
{ standard: 'WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: "Missing slide title")', approximations: "Only the slide-level title field counts. Text that merely looks like a heading inside a block does not." }
|
|
1810
|
+
);
|
|
1811
|
+
var duplicateTitleRule = rule(
|
|
1812
|
+
"duplicate-slide-title",
|
|
1813
|
+
"accessibility",
|
|
1814
|
+
"info",
|
|
1815
|
+
"Two slides have the same title.",
|
|
1816
|
+
"Identical titles make slides indistinguishable in an outline or a screen reader's slide list.",
|
|
1817
|
+
{ approximations: 'Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as "(continued)".' }
|
|
1818
|
+
);
|
|
1819
|
+
var readingOrderRule = rule(
|
|
1820
|
+
"reading-order",
|
|
1821
|
+
"accessibility",
|
|
1822
|
+
"warning",
|
|
1823
|
+
"The order content is read differs from the order it appears on the slide.",
|
|
1824
|
+
"Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.",
|
|
1825
|
+
{
|
|
1826
|
+
standard: 'WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: "Check reading order")',
|
|
1827
|
+
cost: "composition",
|
|
1828
|
+
approximations: "Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared."
|
|
1829
|
+
}
|
|
1830
|
+
);
|
|
1831
|
+
var linkRule = rule(
|
|
1832
|
+
"link-text",
|
|
1833
|
+
"accessibility",
|
|
1834
|
+
"warning",
|
|
1835
|
+
"Link text does not say where the link goes.",
|
|
1836
|
+
`People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. "click here", "read more" or a long raw URL tells them nothing about the destination.`,
|
|
1837
|
+
{ standard: 'WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: "Hyperlink text is not meaningful"', approximations: "Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link." }
|
|
1838
|
+
);
|
|
1839
|
+
var chartColorRule = rule(
|
|
1840
|
+
"chart-color-only",
|
|
1841
|
+
"accessibility",
|
|
1842
|
+
"info",
|
|
1843
|
+
"Chart series may be indistinguishable without colour vision.",
|
|
1844
|
+
"OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.",
|
|
1845
|
+
{
|
|
1846
|
+
standard: "WCAG 2.2 SC 1.4.1 Use of Color, level A",
|
|
1847
|
+
thresholds: ["minSeriesColorDifference"],
|
|
1848
|
+
approximations: "Uses the engine's series palette (ValidateOptions.chartPalette; default chartSeriesPalette of each slide colour scheme (accent1 to accent6, then their tints), adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped."
|
|
1849
|
+
}
|
|
1850
|
+
);
|
|
1851
|
+
var chartAltRule = rule(
|
|
1852
|
+
"chart-text-alternative",
|
|
1853
|
+
"accessibility",
|
|
1854
|
+
"info",
|
|
1855
|
+
"A chart has no text alternative, or is marked decorative.",
|
|
1856
|
+
"A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.",
|
|
1857
|
+
{ standard: "WCAG 2.2 SC 1.1.1 Non-text Content, level A", approximations: `A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: "" is reported as a decorative chart, whatever else is on the slide.` }
|
|
1858
|
+
);
|
|
1859
|
+
var chartColorsRule = rule(
|
|
1860
|
+
"chart-colors-adjusted",
|
|
1861
|
+
"accessibility",
|
|
1862
|
+
"warning",
|
|
1863
|
+
"A chart colour cannot be used as authored.",
|
|
1864
|
+
"`chart.colors` sets the colour of series 1, 2, 3 and so on. A colour that cannot be resolved (a `var:` colour variable the deck does not define, or a name that is not a colour) is ignored and the deck palette colour of that position is used. A colour with under 3:1 contrast against the chart panel is kept for its hue but drawn lighter or darker, so the chart does not show the colour that was written.",
|
|
1865
|
+
{
|
|
1866
|
+
standard: "WCAG 2.2 SC 1.4.11 Non-text Contrast, level AA",
|
|
1867
|
+
approximations: "Resolves each entry like any colour reference (hex, colour-scheme slot or role, `var:` variable) against the slide's scheme and compares it with the chart panel (the slide surface colour) by WCAG contrast; 3:1 is the threshold the engines enforce. Translucent colours and unresolved panels are not measured. Colours that look alike to colour-vision deficiency are reported by opf/chart-color-only, which judges the authored colours."
|
|
1868
|
+
}
|
|
1869
|
+
);
|
|
1870
|
+
var languageRule = rule(
|
|
1871
|
+
"missing-language",
|
|
1872
|
+
"accessibility",
|
|
1873
|
+
"info",
|
|
1874
|
+
"The presentation does not declare its language.",
|
|
1875
|
+
"Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.",
|
|
1876
|
+
{ standard: "WCAG 2.2 SC 3.1.1 Language of Page, level A", approximations: "Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written)." }
|
|
1877
|
+
);
|
|
1878
|
+
var MAX_FURNITURE_LINES = 2;
|
|
1879
|
+
var overflowRule = rule(
|
|
1880
|
+
"text-overflow",
|
|
1881
|
+
"layout",
|
|
1882
|
+
"warning",
|
|
1883
|
+
"Text or a table does not fit its space at the smallest allowed size.",
|
|
1884
|
+
"Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.",
|
|
1885
|
+
{
|
|
1886
|
+
cost: "composition",
|
|
1887
|
+
approximations: "Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit."
|
|
1888
|
+
}
|
|
1889
|
+
);
|
|
1890
|
+
var unresolvedRule = rule(
|
|
1891
|
+
"unresolved-content",
|
|
1892
|
+
"layout",
|
|
1893
|
+
"warning",
|
|
1894
|
+
"Content cannot be drawn as authored.",
|
|
1895
|
+
"composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.",
|
|
1896
|
+
{ cost: "composition" }
|
|
1897
|
+
);
|
|
1898
|
+
var layoutFailedRule = rule(
|
|
1899
|
+
"layout-failed",
|
|
1900
|
+
"layout",
|
|
1901
|
+
"warning",
|
|
1902
|
+
"The slide layout could not be computed.",
|
|
1903
|
+
"Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.",
|
|
1904
|
+
{ cost: "composition" }
|
|
1905
|
+
);
|
|
1906
|
+
var minFontRule = rule(
|
|
1907
|
+
"min-font-size",
|
|
1908
|
+
"layout",
|
|
1909
|
+
"warning",
|
|
1910
|
+
"Text is drawn smaller than the readable minimum.",
|
|
1911
|
+
"Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.",
|
|
1912
|
+
{
|
|
1913
|
+
cost: "composition",
|
|
1914
|
+
standard: "Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).",
|
|
1915
|
+
thresholds: ["minFontSizePt"],
|
|
1916
|
+
approximations: "Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked."
|
|
1917
|
+
}
|
|
1918
|
+
);
|
|
1919
|
+
var regionUnknownRule = rule(
|
|
1920
|
+
"region-unknown",
|
|
1921
|
+
"layout",
|
|
1922
|
+
"warning",
|
|
1923
|
+
"A block is pinned to a region the layout does not have.",
|
|
1924
|
+
"A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.",
|
|
1925
|
+
{ cost: "composition" }
|
|
1926
|
+
);
|
|
1927
|
+
var regionKindRule = rule(
|
|
1928
|
+
"region-kind",
|
|
1929
|
+
"layout",
|
|
1930
|
+
"warning",
|
|
1931
|
+
"A block is pinned to a region that does not accept its kind.",
|
|
1932
|
+
"A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.",
|
|
1933
|
+
{ cost: "composition" }
|
|
1934
|
+
);
|
|
1935
|
+
var regionFullRule = rule(
|
|
1936
|
+
"region-full",
|
|
1937
|
+
"layout",
|
|
1938
|
+
"warning",
|
|
1939
|
+
"A block is pinned to a region that is already full.",
|
|
1940
|
+
"A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.",
|
|
1941
|
+
{ cost: "composition" }
|
|
1942
|
+
);
|
|
1943
|
+
var unplacedRule = rule(
|
|
1944
|
+
"layout-unplaced",
|
|
1945
|
+
"layout",
|
|
1946
|
+
"warning",
|
|
1947
|
+
"No region of the layout has room for a block.",
|
|
1948
|
+
"Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.",
|
|
1949
|
+
{ cost: "composition" }
|
|
1950
|
+
);
|
|
1951
|
+
var furnitureWrapsRule = rule(
|
|
1952
|
+
"furniture-wraps",
|
|
1953
|
+
"layout",
|
|
1954
|
+
"warning",
|
|
1955
|
+
`A header or footer zone wraps onto more than ${MAX_FURNITURE_LINES} lines.`,
|
|
1956
|
+
"Header and footer text repeats on every slide in a band at the slide edge. A zone that wraps onto three or more lines grows into the body, and long source lines read poorly at footer size. Footers carry short repeated text (a title, a date, a page number); sources belong in a caption or the speaker notes.",
|
|
1957
|
+
{
|
|
1958
|
+
cost: "composition",
|
|
1959
|
+
approximations: "Uses the lines core composed for the zone at the deck's slide size; without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimates. A deck-level header or footer is reported once."
|
|
1960
|
+
}
|
|
1961
|
+
);
|
|
1962
|
+
var outsideRule = rule(
|
|
1963
|
+
"font-outside-scheme",
|
|
1964
|
+
"layout",
|
|
1965
|
+
"warning",
|
|
1966
|
+
"Text uses a font family that is not in the deck's font scheme.",
|
|
1967
|
+
"The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.",
|
|
1968
|
+
{ approximations: "Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here." }
|
|
1969
|
+
);
|
|
1970
|
+
var resolutionRule = rule(
|
|
1971
|
+
"image-resolution",
|
|
1972
|
+
"layout",
|
|
1973
|
+
"warning",
|
|
1974
|
+
"An image has too few pixels for the size it is shown at.",
|
|
1975
|
+
"An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.",
|
|
1976
|
+
{
|
|
1977
|
+
cost: "composition",
|
|
1978
|
+
thresholds: ["minImagePpi"],
|
|
1979
|
+
approximations: "Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt."
|
|
1980
|
+
}
|
|
1981
|
+
);
|
|
1982
|
+
var placeholderRule = rule(
|
|
1983
|
+
"placeholder-text",
|
|
1984
|
+
"content",
|
|
1985
|
+
"warning",
|
|
1986
|
+
"Placeholder text was left in the deck.",
|
|
1987
|
+
'"Lorem ipsum", "Click to add title", "TBD" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.',
|
|
1988
|
+
{ approximations: 'Case-insensitive patterns for lorem ipsum, template prompts ("Click to add...", "Your title here", "[Insert ...]", a bare "Title" or "Text"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.' }
|
|
1989
|
+
);
|
|
1990
|
+
var emptyTextRule = rule(
|
|
1991
|
+
"empty-text",
|
|
1992
|
+
"content",
|
|
1993
|
+
"info",
|
|
1994
|
+
"A text field is present but empty.",
|
|
1995
|
+
"An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology."
|
|
1996
|
+
);
|
|
1997
|
+
var emptySlideRule = rule(
|
|
1998
|
+
"empty-slide",
|
|
1999
|
+
"content",
|
|
2000
|
+
"warning",
|
|
2001
|
+
"A slide has no content at all.",
|
|
2002
|
+
"A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)"
|
|
2003
|
+
);
|
|
2004
|
+
var unusedBeatRule = rule(
|
|
2005
|
+
"unused-beat",
|
|
2006
|
+
"content",
|
|
2007
|
+
"info",
|
|
2008
|
+
"A beat of the narrative has no slide that references it.",
|
|
2009
|
+
"The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.",
|
|
2010
|
+
{
|
|
2011
|
+
approximations: "Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them."
|
|
2012
|
+
}
|
|
2013
|
+
);
|
|
2014
|
+
var languageTagRule = rule(
|
|
2015
|
+
"language-tag",
|
|
2016
|
+
"content",
|
|
2017
|
+
"warning",
|
|
2018
|
+
"A language tag is malformed, uses an unassigned region or is not in canonical case.",
|
|
2019
|
+
"Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.",
|
|
2020
|
+
{
|
|
2021
|
+
standard: "BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)",
|
|
2022
|
+
approximations: "Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice."
|
|
2023
|
+
}
|
|
2024
|
+
);
|
|
2025
|
+
var implementedRules = [
|
|
2026
|
+
contrastRule,
|
|
2027
|
+
onImageRule,
|
|
2028
|
+
altRule,
|
|
2029
|
+
poorAltRule,
|
|
2030
|
+
missingTitleRule,
|
|
2031
|
+
duplicateTitleRule,
|
|
2032
|
+
readingOrderRule,
|
|
2033
|
+
linkRule,
|
|
2034
|
+
chartColorRule,
|
|
2035
|
+
chartColorsRule,
|
|
2036
|
+
chartAltRule,
|
|
2037
|
+
languageRule,
|
|
2038
|
+
overflowRule,
|
|
2039
|
+
unresolvedRule,
|
|
2040
|
+
layoutFailedRule,
|
|
2041
|
+
minFontRule,
|
|
2042
|
+
regionUnknownRule,
|
|
2043
|
+
regionKindRule,
|
|
2044
|
+
regionFullRule,
|
|
2045
|
+
unplacedRule,
|
|
2046
|
+
furnitureWrapsRule,
|
|
2047
|
+
outsideRule,
|
|
2048
|
+
resolutionRule,
|
|
2049
|
+
placeholderRule,
|
|
2050
|
+
emptyTextRule,
|
|
2051
|
+
unusedBeatRule,
|
|
2052
|
+
emptySlideRule,
|
|
2053
|
+
languageTagRule
|
|
2054
|
+
];
|
|
2055
|
+
var infos = [...formatRules, ...referenceRules, ...policyRules, ...dataRules, ...implementedRules];
|
|
2056
|
+
var validationRules = Object.freeze(
|
|
2057
|
+
infos.map((info, index) => ({ info, index })).sort((a, b) => validationCategories.indexOf(a.info.category) - validationCategories.indexOf(b.info.category) || a.index - b.index).map(({ info }) => Object.freeze({ ...info }))
|
|
2058
|
+
);
|
|
2059
|
+
var byName = /* @__PURE__ */ new Map();
|
|
2060
|
+
for (const info of validationRules) {
|
|
2061
|
+
if (byName.has(info.id) || byName.has(info.name)) throw new Error(`Duplicate validation rule ${info.id}.`);
|
|
2062
|
+
byName.set(info.id, info);
|
|
2063
|
+
byName.set(info.name, info);
|
|
2064
|
+
}
|
|
2065
|
+
function findValidationRule(name) {
|
|
2066
|
+
return byName.get(name);
|
|
2067
|
+
}
|
|
2068
|
+
function ruleInfo(id) {
|
|
2069
|
+
const found = byName.get(id);
|
|
2070
|
+
if (!found) throw new Error(`Validation rule ${JSON.stringify(id)} is not registered.`);
|
|
2071
|
+
return found;
|
|
2072
|
+
}
|
|
2073
|
+
var VALIDATE_DOCS = "https://github.com/OpenPresentation/opf/blob/main/docs/validate.md";
|
|
2074
|
+
function ruleDocumentation(id) {
|
|
2075
|
+
return `${VALIDATE_DOCS}#${id.replace("/", "")}`;
|
|
1584
2076
|
}
|
|
1585
2077
|
|
|
1586
2078
|
// src/rules-accessibility.ts
|
|
@@ -1683,32 +2175,6 @@ function collectSamples(context) {
|
|
|
1683
2175
|
samples.push({ path: pointerOfDotted(part.sourcePath ?? part.path), color: design.colors.mutedText, sizePx: (part.fit?.fontSize ?? 13 * scale) / scale, bold: false, backdrop: slideBackdrop(context, part.box), source: "default", label: part.kind === "header" ? "Header text" : "Footer text" });
|
|
1684
2176
|
return samples;
|
|
1685
2177
|
}
|
|
1686
|
-
var contrastRule = rule(
|
|
1687
|
-
"text-contrast",
|
|
1688
|
-
"accessibility",
|
|
1689
|
-
"warning",
|
|
1690
|
-
"Text colour has too little contrast against the background it sits on.",
|
|
1691
|
-
"Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.",
|
|
1692
|
-
{
|
|
1693
|
-
standard: "WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA",
|
|
1694
|
-
thresholds: ["contrastNormal", "contrastLarge"],
|
|
1695
|
-
cost: "composition",
|
|
1696
|
-
approximations: "Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour."
|
|
1697
|
-
}
|
|
1698
|
-
);
|
|
1699
|
-
var onImageRule = rule(
|
|
1700
|
-
"text-on-image",
|
|
1701
|
-
"accessibility",
|
|
1702
|
-
"info",
|
|
1703
|
-
"Text sits on a background picture whose pixels cannot be measured.",
|
|
1704
|
-
"Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.",
|
|
1705
|
-
{
|
|
1706
|
-
standard: "WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA",
|
|
1707
|
-
thresholds: ["contrastNormal", "contrastLarge"],
|
|
1708
|
-
cost: "composition",
|
|
1709
|
-
approximations: "Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted."
|
|
1710
|
-
}
|
|
1711
|
-
);
|
|
1712
2178
|
var contrastRules = [
|
|
1713
2179
|
{
|
|
1714
2180
|
info: contrastRule,
|
|
@@ -1769,25 +2235,6 @@ var contrastRules = [
|
|
|
1769
2235
|
}
|
|
1770
2236
|
];
|
|
1771
2237
|
var round = (value) => Math.round(value * 100) / 100;
|
|
1772
|
-
var altRule = rule(
|
|
1773
|
-
"missing-alt-text",
|
|
1774
|
-
"accessibility",
|
|
1775
|
-
"warning",
|
|
1776
|
-
"A picture has no alt text and is not marked decorative.",
|
|
1777
|
-
'People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: ""), which is an explicit, reviewed choice instead of an omission.',
|
|
1778
|
-
{
|
|
1779
|
-
standard: "WCAG 2.2 SC 1.1.1 Non-text Content, level A",
|
|
1780
|
-
approximations: "Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked."
|
|
1781
|
-
}
|
|
1782
|
-
);
|
|
1783
|
-
var poorAltRule = rule(
|
|
1784
|
-
"poor-alt-text",
|
|
1785
|
-
"accessibility",
|
|
1786
|
-
"info",
|
|
1787
|
-
"Alt text is a file name, a URL, a generic word or very long.",
|
|
1788
|
-
'Alt text such as "image", "IMG_2041.png" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.',
|
|
1789
|
-
{ standard: "WCAG 2.2 SC 1.1.1 Non-text Content, level A", approximations: 'Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading "image of", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading "chart of" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.' }
|
|
1790
|
-
);
|
|
1791
2238
|
var GENERIC_ALT = /^(image|picture|photo|photograph|graphic|img|icon|figure|screenshot|untitled|alt|alt text|logo)$/i;
|
|
1792
2239
|
var FILE_ALT = /(\.(png|jpe?g|gif|svg|webp|bmp|tiff?|heic|avif)$)|^(img|dsc|image|screenshot|screen shot|photo|pic)[ _-]?\d+/i;
|
|
1793
2240
|
var GENERIC_CHART_ALT = /^((a|an|the)\s+)?([a-z-]+\s+){0,2}(chart|graph|plot|diagram|figure)$/i;
|
|
@@ -1864,22 +2311,6 @@ var altRules = [
|
|
|
1864
2311
|
}
|
|
1865
2312
|
}
|
|
1866
2313
|
];
|
|
1867
|
-
var missingTitleRule = rule(
|
|
1868
|
-
"missing-slide-title",
|
|
1869
|
-
"accessibility",
|
|
1870
|
-
"warning",
|
|
1871
|
-
"A slide has no title.",
|
|
1872
|
-
"Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.",
|
|
1873
|
-
{ standard: 'WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: "Missing slide title")', approximations: "Only the slide-level title field counts. Text that merely looks like a heading inside a block does not." }
|
|
1874
|
-
);
|
|
1875
|
-
var duplicateTitleRule = rule(
|
|
1876
|
-
"duplicate-slide-title",
|
|
1877
|
-
"accessibility",
|
|
1878
|
-
"info",
|
|
1879
|
-
"Two slides have the same title.",
|
|
1880
|
-
"Identical titles make slides indistinguishable in an outline or a screen reader's slide list.",
|
|
1881
|
-
{ approximations: 'Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as "(continued)".' }
|
|
1882
|
-
);
|
|
1883
2314
|
var titleRules = [
|
|
1884
2315
|
{
|
|
1885
2316
|
info: missingTitleRule,
|
|
@@ -1918,18 +2349,6 @@ var titleRules = [
|
|
|
1918
2349
|
}
|
|
1919
2350
|
}
|
|
1920
2351
|
];
|
|
1921
|
-
var readingOrderRule = rule(
|
|
1922
|
-
"reading-order",
|
|
1923
|
-
"accessibility",
|
|
1924
|
-
"warning",
|
|
1925
|
-
"The order content is read differs from the order it appears on the slide.",
|
|
1926
|
-
"Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.",
|
|
1927
|
-
{
|
|
1928
|
-
standard: 'WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: "Check reading order")',
|
|
1929
|
-
cost: "composition",
|
|
1930
|
-
approximations: "Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared."
|
|
1931
|
-
}
|
|
1932
|
-
);
|
|
1933
2352
|
var readingOrderRules = [
|
|
1934
2353
|
{
|
|
1935
2354
|
info: readingOrderRule,
|
|
@@ -1984,14 +2403,6 @@ function visualOrder(items, rtl) {
|
|
|
1984
2403
|
}
|
|
1985
2404
|
var shortField = (path) => path.replace(/^slides\.\d+\./, "");
|
|
1986
2405
|
var ordinal = (index) => `position ${index + 1}`;
|
|
1987
|
-
var linkRule = rule(
|
|
1988
|
-
"link-text",
|
|
1989
|
-
"accessibility",
|
|
1990
|
-
"warning",
|
|
1991
|
-
"Link text does not say where the link goes.",
|
|
1992
|
-
`People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. "click here", "read more" or a long raw URL tells them nothing about the destination.`,
|
|
1993
|
-
{ standard: 'WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: "Hyperlink text is not meaningful"', approximations: "Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link." }
|
|
1994
|
-
);
|
|
1995
2406
|
var GENERIC_LINKS = /* @__PURE__ */ new Set(["click here", "click", "here", "link", "this link", "this", "read more", "more", "learn more", "see more", "more info", "more information", "details", "download", "url", "website", "web site", "page", "this page", "visit", "go"]);
|
|
1996
2407
|
var linkRules = [
|
|
1997
2408
|
{
|
|
@@ -2024,37 +2435,6 @@ var linkRules = [
|
|
|
2024
2435
|
}
|
|
2025
2436
|
}
|
|
2026
2437
|
];
|
|
2027
|
-
var chartColorRule = rule(
|
|
2028
|
-
"chart-color-only",
|
|
2029
|
-
"accessibility",
|
|
2030
|
-
"info",
|
|
2031
|
-
"Chart series may be indistinguishable without colour vision.",
|
|
2032
|
-
"OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.",
|
|
2033
|
-
{
|
|
2034
|
-
standard: "WCAG 2.2 SC 1.4.1 Use of Color, level A",
|
|
2035
|
-
thresholds: ["minSeriesColorDifference"],
|
|
2036
|
-
approximations: "Uses the engine's series palette (ValidateOptions.chartPalette; default chartSeriesPalette of each slide colour scheme (accent1 to accent6, then their tints), adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped."
|
|
2037
|
-
}
|
|
2038
|
-
);
|
|
2039
|
-
var chartAltRule = rule(
|
|
2040
|
-
"chart-text-alternative",
|
|
2041
|
-
"accessibility",
|
|
2042
|
-
"info",
|
|
2043
|
-
"A chart has no text alternative, or is marked decorative.",
|
|
2044
|
-
"A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.",
|
|
2045
|
-
{ standard: "WCAG 2.2 SC 1.1.1 Non-text Content, level A", approximations: `A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: "" is reported as a decorative chart, whatever else is on the slide.` }
|
|
2046
|
-
);
|
|
2047
|
-
var chartColorsRule = rule(
|
|
2048
|
-
"chart-colors-adjusted",
|
|
2049
|
-
"accessibility",
|
|
2050
|
-
"warning",
|
|
2051
|
-
"A chart colour cannot be used as authored.",
|
|
2052
|
-
"`chart.colors` sets the colour of series 1, 2, 3 and so on. A colour that cannot be resolved (a `var:` colour variable the deck does not define, or a name that is not a colour) is ignored and the deck palette colour of that position is used. A colour with under 3:1 contrast against the chart panel is kept for its hue but drawn lighter or darker, so the chart does not show the colour that was written.",
|
|
2053
|
-
{
|
|
2054
|
-
standard: "WCAG 2.2 SC 1.4.11 Non-text Contrast, level AA",
|
|
2055
|
-
approximations: "Resolves each entry like any colour reference (hex, colour-scheme slot or role, `var:` variable) against the slide's scheme and compares it with the chart panel (the slide surface colour) by WCAG contrast; 3:1 is the threshold the engines enforce. Translucent colours and unresolved panels are not measured. Colours that look alike to colour-vision deficiency are reported by opf/chart-color-only, which judges the authored colours."
|
|
2056
|
-
}
|
|
2057
|
-
);
|
|
2058
2438
|
var chartColorContext = (design) => ({
|
|
2059
2439
|
roles: { primary: design.colors.primary, secondary: design.colors.secondary, accent: design.colors.accent, background: design.colors.background, surface: design.colors.surface, text: design.colors.text, textSecondary: design.colors.mutedText },
|
|
2060
2440
|
variables: design.variables
|
|
@@ -2178,14 +2558,6 @@ var chartRules = [
|
|
|
2178
2558
|
}
|
|
2179
2559
|
}
|
|
2180
2560
|
];
|
|
2181
|
-
var languageRule = rule(
|
|
2182
|
-
"missing-language",
|
|
2183
|
-
"accessibility",
|
|
2184
|
-
"info",
|
|
2185
|
-
"The presentation does not declare its language.",
|
|
2186
|
-
"Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.",
|
|
2187
|
-
{ standard: "WCAG 2.2 SC 3.1.1 Language of Page, level A", approximations: "Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written)." }
|
|
2188
|
-
);
|
|
2189
2561
|
var languageRules = [
|
|
2190
2562
|
{
|
|
2191
2563
|
info: languageRule,
|
|
@@ -2270,17 +2642,6 @@ function languageTagProblem(tag) {
|
|
|
2270
2642
|
}
|
|
2271
2643
|
|
|
2272
2644
|
// src/rules-language.ts
|
|
2273
|
-
var languageTagRule = rule(
|
|
2274
|
-
"language-tag",
|
|
2275
|
-
"content",
|
|
2276
|
-
"warning",
|
|
2277
|
-
"A language tag is malformed, uses an unassigned region or is not in canonical case.",
|
|
2278
|
-
"Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.",
|
|
2279
|
-
{
|
|
2280
|
-
standard: "BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)",
|
|
2281
|
-
approximations: "Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice."
|
|
2282
|
-
}
|
|
2283
|
-
);
|
|
2284
2645
|
var report = (context, tag, path, slide) => {
|
|
2285
2646
|
if (typeof tag !== "string") return;
|
|
2286
2647
|
const problem = languageTagProblem(tag);
|
|
@@ -2304,99 +2665,15 @@ var languageTagRules = [
|
|
|
2304
2665
|
} else if (language !== void 0 && !isEnUk(rec(language).bcp47)) report(context, rec(language).bcp47, "/language/bcp47");
|
|
2305
2666
|
for (const slide of context.slides)
|
|
2306
2667
|
for (const tv of slide.texts)
|
|
2307
|
-
if (typeof tv.value !== "string") for (const run of runsOf(tv)) report(context, run.style.lang, `${run.path}/lang`, slide);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
];
|
|
2311
|
-
|
|
2312
|
-
// src/rules-layout-content.ts
|
|
2313
|
-
var PX_TO_PT2 = 0.75;
|
|
2314
|
-
var round2 = (value, places = 1) => Math.round(value * 10 ** places) / 10 ** places;
|
|
2315
|
-
var overflowRule = rule(
|
|
2316
|
-
"text-overflow",
|
|
2317
|
-
"layout",
|
|
2318
|
-
"warning",
|
|
2319
|
-
"Text or a table does not fit its space at the smallest allowed size.",
|
|
2320
|
-
"Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.",
|
|
2321
|
-
{
|
|
2322
|
-
cost: "composition",
|
|
2323
|
-
approximations: "Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit."
|
|
2324
|
-
}
|
|
2325
|
-
);
|
|
2326
|
-
var unresolvedRule = rule(
|
|
2327
|
-
"unresolved-content",
|
|
2328
|
-
"layout",
|
|
2329
|
-
"warning",
|
|
2330
|
-
"Content cannot be drawn as authored.",
|
|
2331
|
-
"composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.",
|
|
2332
|
-
{ cost: "composition" }
|
|
2333
|
-
);
|
|
2334
|
-
var layoutFailedRule = rule(
|
|
2335
|
-
"layout-failed",
|
|
2336
|
-
"layout",
|
|
2337
|
-
"warning",
|
|
2338
|
-
"The slide layout could not be computed.",
|
|
2339
|
-
"Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.",
|
|
2340
|
-
{ cost: "composition" }
|
|
2341
|
-
);
|
|
2342
|
-
var minFontRule = rule(
|
|
2343
|
-
"min-font-size",
|
|
2344
|
-
"layout",
|
|
2345
|
-
"warning",
|
|
2346
|
-
"Text is drawn smaller than the readable minimum.",
|
|
2347
|
-
"Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.",
|
|
2348
|
-
{
|
|
2349
|
-
cost: "composition",
|
|
2350
|
-
standard: "Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).",
|
|
2351
|
-
thresholds: ["minFontSizePt"],
|
|
2352
|
-
approximations: "Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked."
|
|
2353
|
-
}
|
|
2354
|
-
);
|
|
2355
|
-
var regionUnknownRule = rule(
|
|
2356
|
-
"region-unknown",
|
|
2357
|
-
"layout",
|
|
2358
|
-
"warning",
|
|
2359
|
-
"A block is pinned to a region the layout does not have.",
|
|
2360
|
-
"A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.",
|
|
2361
|
-
{ cost: "composition" }
|
|
2362
|
-
);
|
|
2363
|
-
var regionKindRule = rule(
|
|
2364
|
-
"region-kind",
|
|
2365
|
-
"layout",
|
|
2366
|
-
"warning",
|
|
2367
|
-
"A block is pinned to a region that does not accept its kind.",
|
|
2368
|
-
"A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.",
|
|
2369
|
-
{ cost: "composition" }
|
|
2370
|
-
);
|
|
2371
|
-
var regionFullRule = rule(
|
|
2372
|
-
"region-full",
|
|
2373
|
-
"layout",
|
|
2374
|
-
"warning",
|
|
2375
|
-
"A block is pinned to a region that is already full.",
|
|
2376
|
-
"A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.",
|
|
2377
|
-
{ cost: "composition" }
|
|
2378
|
-
);
|
|
2379
|
-
var unplacedRule = rule(
|
|
2380
|
-
"layout-unplaced",
|
|
2381
|
-
"layout",
|
|
2382
|
-
"warning",
|
|
2383
|
-
"No region of the layout has room for a block.",
|
|
2384
|
-
"Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.",
|
|
2385
|
-
{ cost: "composition" }
|
|
2386
|
-
);
|
|
2387
|
-
var REGION_RULES = { "region-unknown": regionUnknownRule, "region-kind": regionKindRule, "region-full": regionFullRule, "layout-unplaced": unplacedRule };
|
|
2388
|
-
var MAX_FURNITURE_LINES = 2;
|
|
2389
|
-
var furnitureWrapsRule = rule(
|
|
2390
|
-
"furniture-wraps",
|
|
2391
|
-
"layout",
|
|
2392
|
-
"warning",
|
|
2393
|
-
`A header or footer zone wraps onto more than ${MAX_FURNITURE_LINES} lines.`,
|
|
2394
|
-
"Header and footer text repeats on every slide in a band at the slide edge. A zone that wraps onto three or more lines grows into the body, and long source lines read poorly at footer size. Footers carry short repeated text (a title, a date, a page number); sources belong in a caption or the speaker notes.",
|
|
2395
|
-
{
|
|
2396
|
-
cost: "composition",
|
|
2397
|
-
approximations: "Uses the lines core composed for the zone at the deck's slide size; without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimates. A deck-level header or footer is reported once."
|
|
2668
|
+
if (typeof tv.value !== "string") for (const run of runsOf(tv)) report(context, run.style.lang, `${run.path}/lang`, slide);
|
|
2669
|
+
}
|
|
2398
2670
|
}
|
|
2399
|
-
|
|
2671
|
+
];
|
|
2672
|
+
|
|
2673
|
+
// src/rules-layout-content.ts
|
|
2674
|
+
var PX_TO_PT2 = 0.75;
|
|
2675
|
+
var round2 = (value, places = 1) => Math.round(value * 10 ** places) / 10 ** places;
|
|
2676
|
+
var REGION_RULES = { "region-unknown": regionUnknownRule, "region-kind": regionKindRule, "region-full": regionFullRule, "layout-unplaced": unplacedRule };
|
|
2400
2677
|
var layoutRules = [
|
|
2401
2678
|
{
|
|
2402
2679
|
info: overflowRule,
|
|
@@ -2477,14 +2754,6 @@ var layoutRules = [
|
|
|
2477
2754
|
}
|
|
2478
2755
|
}
|
|
2479
2756
|
];
|
|
2480
|
-
var outsideRule = rule(
|
|
2481
|
-
"font-outside-scheme",
|
|
2482
|
-
"layout",
|
|
2483
|
-
"warning",
|
|
2484
|
-
"Text uses a font family that is not in the deck's font scheme.",
|
|
2485
|
-
"The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.",
|
|
2486
|
-
{ approximations: "Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here." }
|
|
2487
|
-
);
|
|
2488
2757
|
var fontRules = [
|
|
2489
2758
|
{
|
|
2490
2759
|
info: outsideRule,
|
|
@@ -2511,18 +2780,6 @@ var fontRules = [
|
|
|
2511
2780
|
}
|
|
2512
2781
|
}
|
|
2513
2782
|
];
|
|
2514
|
-
var resolutionRule = rule(
|
|
2515
|
-
"image-resolution",
|
|
2516
|
-
"layout",
|
|
2517
|
-
"warning",
|
|
2518
|
-
"An image has too few pixels for the size it is shown at.",
|
|
2519
|
-
"An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.",
|
|
2520
|
-
{
|
|
2521
|
-
cost: "composition",
|
|
2522
|
-
thresholds: ["minImagePpi"],
|
|
2523
|
-
approximations: "Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt."
|
|
2524
|
-
}
|
|
2525
|
-
);
|
|
2526
2783
|
var resolutionRules = [
|
|
2527
2784
|
{
|
|
2528
2785
|
info: resolutionRule,
|
|
@@ -2556,38 +2813,6 @@ var resolutionRules = [
|
|
|
2556
2813
|
}
|
|
2557
2814
|
}
|
|
2558
2815
|
];
|
|
2559
|
-
var placeholderRule = rule(
|
|
2560
|
-
"placeholder-text",
|
|
2561
|
-
"content",
|
|
2562
|
-
"warning",
|
|
2563
|
-
"Placeholder text was left in the deck.",
|
|
2564
|
-
'"Lorem ipsum", "Click to add title", "TBD" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.',
|
|
2565
|
-
{ approximations: 'Case-insensitive patterns for lorem ipsum, template prompts ("Click to add...", "Your title here", "[Insert ...]", a bare "Title" or "Text"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.' }
|
|
2566
|
-
);
|
|
2567
|
-
var emptyTextRule = rule(
|
|
2568
|
-
"empty-text",
|
|
2569
|
-
"content",
|
|
2570
|
-
"info",
|
|
2571
|
-
"A text field is present but empty.",
|
|
2572
|
-
"An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology."
|
|
2573
|
-
);
|
|
2574
|
-
var emptySlideRule = rule(
|
|
2575
|
-
"empty-slide",
|
|
2576
|
-
"content",
|
|
2577
|
-
"warning",
|
|
2578
|
-
"A slide has no content at all.",
|
|
2579
|
-
"A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)"
|
|
2580
|
-
);
|
|
2581
|
-
var unusedBeatRule = rule(
|
|
2582
|
-
"unused-beat",
|
|
2583
|
-
"content",
|
|
2584
|
-
"info",
|
|
2585
|
-
"A beat of the narrative has no slide that references it.",
|
|
2586
|
-
"The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.",
|
|
2587
|
-
{
|
|
2588
|
-
approximations: "Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them."
|
|
2589
|
-
}
|
|
2590
|
-
);
|
|
2591
2816
|
var PLACEHOLDERS = [
|
|
2592
2817
|
{ pattern: /lorem ipsum|dolor sit amet|consectetur adipiscing/i, label: "lorem ipsum" },
|
|
2593
2818
|
{ pattern: /\b(click|tap) to (add|edit|enter)\b|\b(add|enter|insert|type) (a |your |the )?(title|subtitle|text|heading|caption|content|name|description)( here)?\s*$/i, label: "a template prompt" },
|
|
@@ -2669,208 +2894,8 @@ var layoutContentRules = [
|
|
|
2669
2894
|
...contentRules
|
|
2670
2895
|
];
|
|
2671
2896
|
|
|
2672
|
-
// src/validation-types.ts
|
|
2673
|
-
var validationCategories = Object.freeze([
|
|
2674
|
-
"format",
|
|
2675
|
-
"references",
|
|
2676
|
-
"policy",
|
|
2677
|
-
"accessibility",
|
|
2678
|
-
"layout",
|
|
2679
|
-
"content"
|
|
2680
|
-
]);
|
|
2681
|
-
|
|
2682
2897
|
// src/validation-rules.ts
|
|
2683
|
-
var
|
|
2684
|
-
contrastNormal: 4.5,
|
|
2685
|
-
contrastLarge: 3,
|
|
2686
|
-
minFontSizePt: 11,
|
|
2687
|
-
minImagePpi: 96,
|
|
2688
|
-
minSeriesColorDifference: 10
|
|
2689
|
-
});
|
|
2690
|
-
var SCHEMA = "spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF";
|
|
2691
|
-
var formatRules = [
|
|
2692
|
-
rule(
|
|
2693
|
-
"json-syntax",
|
|
2694
|
-
"format",
|
|
2695
|
-
"error",
|
|
2696
|
-
"The text is not strict JSON.",
|
|
2697
|
-
"Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.",
|
|
2698
|
-
{ cost: "syntax", standard: "RFC 8259 (strict JSON: no comments, no trailing commas)", approximations: "Only text input is checked. A string is always read as JSON. One finding per root cause, in position order and at most five (a key with no value, a missing comma, a bracket never closed; not the damage each one causes further on): `message` says what is wrong, `help` what to do next and `location` is the range to underline. A text that looks like YAML or Markdown gets one finding with a hint to use fromYaml or fromMarkdown first. Source editors get the same causes with their quick fixes from analyzeJsonSyntax (@openpresentation/opf/source)." }
|
|
2699
|
-
),
|
|
2700
|
-
rule(
|
|
2701
|
-
"duplicate-key",
|
|
2702
|
-
"format",
|
|
2703
|
-
"error",
|
|
2704
|
-
"A JSON object repeats a key.",
|
|
2705
|
-
"JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.",
|
|
2706
|
-
{ cost: "syntax", standard: "RFC 8259 section 4 (names within an object should be unique)" }
|
|
2707
|
-
),
|
|
2708
|
-
rule(
|
|
2709
|
-
"schema",
|
|
2710
|
-
"format",
|
|
2711
|
-
"error",
|
|
2712
|
-
"The document breaks the OPF schema or a structural rule the schema cannot express.",
|
|
2713
|
-
"The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.",
|
|
2714
|
-
{ standard: SCHEMA, approximations: "All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category." }
|
|
2715
|
-
),
|
|
2716
|
-
rule("reference-id-duplicate", "format", "error", "Two references share an id.", "A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.", { standard: SCHEMA }),
|
|
2717
|
-
rule("cite-unknown-reference", "format", "error", "A citation names a reference that does not exist.", "A cited id with no entry in `references` has nothing to show in the footnote area.", { standard: SCHEMA }),
|
|
2718
|
-
rule("cite-unsupported-location", "format", "error", "A citation or footnote sits where no engine draws it.", "`cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.", { standard: SCHEMA }),
|
|
2719
|
-
rule("caption-unsupported-payload", "format", "error", "A caption is on a payload that cannot carry one.", "A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.", { standard: SCHEMA }),
|
|
2720
|
-
rule("image-option-unsupported-payload", "format", "error", "An image option is on a payload that is not an image.", "`fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.", { standard: SCHEMA }),
|
|
2721
|
-
rule("image-placement-invalid", "format", "error", "An image placement cannot be honoured.", "A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.", { standard: SCHEMA }),
|
|
2722
|
-
rule("layout-placeholder-group", "format", "error", "A layout record's placeholder group breaks a nesting rule.", "A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.", { standard: SCHEMA }),
|
|
2723
|
-
rule("dataset-unknown", "format", "error", "A chart or table names a dataset that does not exist.", "The data cannot be drawn without the dataset it refers to.", { standard: SCHEMA }),
|
|
2724
|
-
rule("dataset-field-unknown", "format", "error", "A table asks a dataset for a column it does not have.", "The column cannot be shown when the dataset has no column of that name.", { standard: SCHEMA }),
|
|
2725
|
-
rule("data-column-duplicate", "format", "error", "A dataset or table has two columns with the same name.", "Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.", { standard: SCHEMA }),
|
|
2726
|
-
rule("chart-mapping-unknown-column", "format", "error", "A chart mapping names a column the data does not have.", "The series cannot be built from a column that is not there.", { standard: SCHEMA }),
|
|
2727
|
-
rule("number-format-invalid", "format", "error", "A number format cannot be read.", "A format code that no engine can parse is drawn as the plain number or fails to export.", { standard: "Excel number format codes, as far as OPF supports them" }),
|
|
2728
|
-
rule("variable-invalid-value", "format", "error", "A variable value does not match its declared type.", "A number variable given text, or a date given a non-date, cannot be filled into the deck.", { standard: SCHEMA }),
|
|
2729
|
-
rule("variable-unknown-builtin", "format", "error", "A token names a built-in variable that does not exist.", "The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.", { standard: SCHEMA }),
|
|
2730
|
-
rule("chart-highlight-unknown-name", "format", "error", "A chart highlight names a series or category the chart does not have.", "`chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.", { standard: SCHEMA }),
|
|
2731
|
-
rule("variable-format", "format", "error", "A variable cannot be formatted.", "A number or date format that does not apply to the value leaves the token unreadable.", { standard: SCHEMA }),
|
|
2732
|
-
rule(
|
|
2733
|
-
"variable-unfilled",
|
|
2734
|
-
"format",
|
|
2735
|
-
"error",
|
|
2736
|
-
"A required template variable has no value.",
|
|
2737
|
-
"A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.",
|
|
2738
|
-
{
|
|
2739
|
-
approximations: "Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings."
|
|
2740
|
-
}
|
|
2741
|
-
),
|
|
2742
|
-
rule("run-color-unrecognized", "format", "warning", "A text run colour is none of the documented forms.", "Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.", { standard: SCHEMA }),
|
|
2743
|
-
rule("numbering-start-ignored", "format", "warning", "A list entry sets `start` where nothing is numbered.", "A start value only restarts an auto-number; without `numbering` on the payload it has no effect.", { standard: SCHEMA }),
|
|
2744
|
-
rule(
|
|
2745
|
-
"slide-size-out-of-range",
|
|
2746
|
-
"format",
|
|
2747
|
-
"warning",
|
|
2748
|
-
"The slide size is outside PowerPoint's 1 to 56 inch range.",
|
|
2749
|
-
"The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.",
|
|
2750
|
-
{
|
|
2751
|
-
standard: "ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU",
|
|
2752
|
-
approximations: "Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured."
|
|
2753
|
-
}
|
|
2754
|
-
)
|
|
2755
|
-
];
|
|
2756
|
-
var referenceRules = [
|
|
2757
|
-
rule(
|
|
2758
|
-
"asset-reference",
|
|
2759
|
-
"references",
|
|
2760
|
-
"error",
|
|
2761
|
-
"An `asset:` reference names an asset that is not in the registry.",
|
|
2762
|
-
"An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.",
|
|
2763
|
-
{ standard: SCHEMA }
|
|
2764
|
-
),
|
|
2765
|
-
rule("asset-cycle", "references", "error", "Asset references form a cycle.", "An asset whose source points back at itself never resolves to a file.", { standard: SCHEMA }),
|
|
2766
|
-
rule(
|
|
2767
|
-
"catalog-record",
|
|
2768
|
-
"references",
|
|
2769
|
-
"error",
|
|
2770
|
-
"An embedded or registered catalog record is invalid.",
|
|
2771
|
-
"A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.",
|
|
2772
|
-
{ standard: "spec/schemas/<kind>.schema.json" }
|
|
2773
|
-
),
|
|
2774
|
-
rule(
|
|
2775
|
-
"layout-template",
|
|
2776
|
-
"references",
|
|
2777
|
-
"error",
|
|
2778
|
-
"A layout template's region list is malformed.",
|
|
2779
|
-
`A layout is one ordered list of regions, nested through splits for two dimensions. Engines cannot compose a list that is not 1 to 12 entries, an entry that is neither a region nor a split, a direction or size that is not valid, an "auto" size anywhere but in a column, splits nested more than three levels deep (the record's own list is level 1), or regions that compile to a grid larger than 12 x 12. composeSlide refuses such a record, so a slide that names it composes automatically.`,
|
|
2780
|
-
{ standard: "spec/schemas/layout.schema.json and the region list rules (docs/layout-templates.md)", approximations: "Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema." }
|
|
2781
|
-
),
|
|
2782
|
-
rule(
|
|
2783
|
-
"layout-region",
|
|
2784
|
-
"references",
|
|
2785
|
-
"error",
|
|
2786
|
-
"A layout template's regions are not valid.",
|
|
2787
|
-
"Region names are unique across the whole record and may not be tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; title and subtitle name the heading regions, which take only a size; a subtitle region needs a title region; a region accepts at least one content kind; a region with flow 'none' holds one block; overflowRegion must name a body region. composeSlide refuses such a record.",
|
|
2788
|
-
{ standard: "spec/schemas/layout.schema.json and the region rules (docs/layout-templates.md)" }
|
|
2789
|
-
),
|
|
2790
|
-
rule(
|
|
2791
|
-
"unresolved-reference",
|
|
2792
|
-
"references",
|
|
2793
|
-
"warning",
|
|
2794
|
-
"A content reference resolves nowhere.",
|
|
2795
|
-
"A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.",
|
|
2796
|
-
{ standard: SCHEMA, approximations: "Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference." }
|
|
2797
|
-
),
|
|
2798
|
-
rule(
|
|
2799
|
-
"catalog-record-not-in-source",
|
|
2800
|
-
"references",
|
|
2801
|
-
"warning",
|
|
2802
|
-
"A record embedded under a catalog group is not in that catalog.",
|
|
2803
|
-
"A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.",
|
|
2804
|
-
{ standard: SCHEMA, approximations: "Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked." }
|
|
2805
|
-
),
|
|
2806
|
-
rule("undeclared-catalog", "format", "error", "A reference names a catalog group the document does not declare.", "The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.", { standard: SCHEMA }),
|
|
2807
|
-
rule("unused-reference", "references", "warning", "A reference is never cited.", "A listed reference that no run cites is not drawn anywhere.", { standard: SCHEMA }),
|
|
2808
|
-
rule("unused-dataset", "references", "warning", "A dataset is never used.", "A dataset no chart or table references is not drawn anywhere.", { standard: SCHEMA }),
|
|
2809
|
-
rule("unknown-beat", "references", "warning", "A slide names a beat its narrative does not define.", "`slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.", { standard: SCHEMA, approximations: "The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked." }),
|
|
2810
|
-
rule("slide-theme-dimensions", "references", "warning", "A slide theme sets a slide size that differs from the deck.", "A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.", { standard: SCHEMA }),
|
|
2811
|
-
rule("variable-unknown", "references", "warning", "A `{{token}}` names a variable that is not declared.", "An undeclared token is shown literally.", { standard: SCHEMA }),
|
|
2812
|
-
rule("variable-unknown-value", "references", "warning", "A value was supplied for a variable that is not declared.", "The value is never used.", { standard: SCHEMA }),
|
|
2813
|
-
rule("variable-unused", "references", "warning", "A variable is declared but never used.", "A declaration nothing refers to has no effect.", { standard: SCHEMA }),
|
|
2814
|
-
rule("variable-reference-unknown", "references", "warning", "A `var:` colour names a variable that is not declared.", "An undeclared colour variable falls back to a default colour.", { standard: SCHEMA })
|
|
2815
|
-
];
|
|
2816
|
-
var policyRules = [
|
|
2817
|
-
rule(
|
|
2818
|
-
"contract",
|
|
2819
|
-
"policy",
|
|
2820
|
-
"error",
|
|
2821
|
-
"A field is outside the values a host policy allows.",
|
|
2822
|
-
"A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.",
|
|
2823
|
-
{ approximations: "Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default." }
|
|
2824
|
-
)
|
|
2825
|
-
];
|
|
2826
|
-
var dataRules = [
|
|
2827
|
-
rule("chart-option-adapted", "layout", "warning", "A chart option cannot be shown by the chart type.", "An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.", { standard: SCHEMA }),
|
|
2828
|
-
rule(
|
|
2829
|
-
"chart-value-not-numeric",
|
|
2830
|
-
"content",
|
|
2831
|
-
"warning",
|
|
2832
|
-
"A chart value is text, not a number.",
|
|
2833
|
-
'A chart plots numbers. A text cell such as "12%" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.',
|
|
2834
|
-
{ standard: SCHEMA }
|
|
2835
|
-
),
|
|
2836
|
-
rule("chart-mapping-adapted", "content", "warning", "A chart mapping entry is ignored.", "A mapping that names a column the chart type cannot use is left out of the chart.", { standard: SCHEMA }),
|
|
2837
|
-
rule("chart-highlight-adapted", "content", "warning", "A chart highlight is adapted by the chart type.", "A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.", { standard: SCHEMA }),
|
|
2838
|
-
rule("code-highlight-out-of-range", "content", "warning", "A code highlight marks a line the code does not have.", "Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.", { standard: SCHEMA }),
|
|
2839
|
-
rule("code-highlight-range-reversed", "content", "warning", "A code highlight range ends before it starts.", "A range written end before start marks nothing; write the smaller line number first.", { standard: SCHEMA }),
|
|
2840
|
-
rule("variable-builtin-missing", "content", "warning", "A built-in variable has no value in this document.", "A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.", { standard: SCHEMA }),
|
|
2841
|
-
rule("duration-outside-narrative", "content", "warning", "The target duration is outside the range the narrative suits.", "A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.", { standard: SCHEMA }),
|
|
2842
|
-
rule("narrative-duration-range", "content", "warning", "An inline narrative duration range has min greater than max.", "A range with its bounds swapped matches no duration, so the duration check cannot help.", { standard: "spec/schemas/narrative.schema.json" })
|
|
2843
|
-
];
|
|
2844
|
-
var implementations = [...accessibilityRules, ...layoutContentRules, ...languageTagRules];
|
|
2845
|
-
var ruleImplementations = implementations;
|
|
2846
|
-
var infos = [
|
|
2847
|
-
...formatRules,
|
|
2848
|
-
...referenceRules,
|
|
2849
|
-
...policyRules,
|
|
2850
|
-
...dataRules,
|
|
2851
|
-
...implementations.flatMap((entry2) => [entry2.info, ...entry2.also ?? []])
|
|
2852
|
-
];
|
|
2853
|
-
var validationRules = Object.freeze(
|
|
2854
|
-
infos.map((info, index) => ({ info, index })).sort((a, b) => validationCategories.indexOf(a.info.category) - validationCategories.indexOf(b.info.category) || a.index - b.index).map(({ info }) => Object.freeze({ ...info }))
|
|
2855
|
-
);
|
|
2856
|
-
var byName = /* @__PURE__ */ new Map();
|
|
2857
|
-
for (const info of validationRules) {
|
|
2858
|
-
if (byName.has(info.id) || byName.has(info.name)) throw new Error(`Duplicate validation rule ${info.id}.`);
|
|
2859
|
-
byName.set(info.id, info);
|
|
2860
|
-
byName.set(info.name, info);
|
|
2861
|
-
}
|
|
2862
|
-
function findValidationRule(name) {
|
|
2863
|
-
return byName.get(name);
|
|
2864
|
-
}
|
|
2865
|
-
function ruleInfo(id) {
|
|
2866
|
-
const found = byName.get(id);
|
|
2867
|
-
if (!found) throw new Error(`Validation rule ${JSON.stringify(id)} is not registered.`);
|
|
2868
|
-
return found;
|
|
2869
|
-
}
|
|
2870
|
-
var VALIDATE_DOCS = "https://github.com/OpenPresentation/opf/blob/main/docs/validate.md";
|
|
2871
|
-
function ruleDocumentation(id) {
|
|
2872
|
-
return `${VALIDATE_DOCS}#${id.replace("/", "")}`;
|
|
2873
|
-
}
|
|
2898
|
+
var ruleImplementations = [...accessibilityRules, ...layoutContentRules, ...languageTagRules];
|
|
2874
2899
|
|
|
2875
2900
|
// src/check-document.ts
|
|
2876
2901
|
var own = (value, key2) => Object.hasOwn(value, key2);
|
|
@@ -5384,6 +5409,14 @@ function fieldLabel(segments) {
|
|
|
5384
5409
|
}
|
|
5385
5410
|
return code(last);
|
|
5386
5411
|
}
|
|
5412
|
+
function placeWords(segments) {
|
|
5413
|
+
if (!segments.length) return "at the top level";
|
|
5414
|
+
const slide = segments[0] === "slides" && segments.length >= 2 && isIndex(segments[1]) ? Number(segments[1]) + 1 : void 0;
|
|
5415
|
+
if (slide !== void 0 && segments.length === 2) return `in slide ${slide}`;
|
|
5416
|
+
const last = segments[segments.length - 1];
|
|
5417
|
+
const inner = isIndex(last) ? fieldLabel(segments) : code(last);
|
|
5418
|
+
return slide !== void 0 ? `in ${inner} of slide ${slide}` : `in ${inner}`;
|
|
5419
|
+
}
|
|
5387
5420
|
function containerName(segments) {
|
|
5388
5421
|
if (!segments.length) return "the deck";
|
|
5389
5422
|
if (segments.length === 2 && segments[0] === "slides" && isIndex(segments[1])) return "a slide";
|
|
@@ -6007,7 +6040,7 @@ var jsonSyntax = (finding2) => ({
|
|
|
6007
6040
|
});
|
|
6008
6041
|
var duplicateKey = (finding2, _context, helpers) => {
|
|
6009
6042
|
const key2 = helpers.segments.at(-1) ?? unquote(match(finding2.message, /^Duplicate JSON property (".*")\.$/)?.[1]);
|
|
6010
|
-
const owner =
|
|
6043
|
+
const owner = placeWords(helpers.segments.slice(0, -1));
|
|
6011
6044
|
return {
|
|
6012
6045
|
message: `${code(key2)} appears twice ${owner}. JSON keeps only the last value, so the earlier one is ignored.`,
|
|
6013
6046
|
next: "keep one of them, or merge their values into one."
|