diff --git a/docs/common/src/utils/rehype-sls-ids.mjs b/docs/common/src/utils/rehype-sls-ids.mjs index 07c09e09f8c..f6e7503e6e9 100644 --- a/docs/common/src/utils/rehype-sls-ids.mjs +++ b/docs/common/src/utils/rehype-sls-ids.mjs @@ -19,17 +19,21 @@ // comments, and whatever else grows one) free to write them unconditionally. // // The pages that do carry identifiers are the language specification and, in -// the safety manual, the generated SC API reference: pass -// `{ generatedReferenceRequiresIds: true }` for the latter. Those pages are +// the safety manual, everything under `reference/`: the generated SC API +// reference and the chapters the manual writes itself, like rendering and +// generated code. Pass `{ referenceRequiresIds: true }` for those. They are // also checked for completeness -- a normative paragraph without an // identifier fails the build -- covering top-level paragraphs of the -// specification (nested ones are asides and list items) and every paragraph -// of the generated reference. A specification chapter with `notInSC: true` -// in its frontmatter covers the full language only: the safety manual leaves -// it out, so it states no requirements and its markers are dropped like on -// any other page that carries no identifiers -- an anchor there would -// dead-link from the traceability matrix. The markers stay in the source for -// when the chapter joins the subset. +// specification and of the manual's own chapters (nested ones are asides and +// list items) and every paragraph of the generated and property-types +// reference. A page states no requirements, and so drops its markers instead +// of assigning them, in two cases: a specification chapter with +// `notInSC: true` covers the full language only, so the safety manual leaves +// it out, and a page with `normative: false` is navigational, like a section +// landing page. Dropping keeps the corpus and the traceability matrix in +// step: the matrix cites neither kind of page, and an anchor it never cites +// dead-links from nowhere and hides a marker that should have been checked. +// The markers stay in the source for when a chapter joins the subset. // // The same marker format lives in `split_marker` in // docs/slint-doc-generator/traceability.rs and in the `.sls-id` styling in @@ -51,6 +55,12 @@ const SPEC_PATH = /[\\/]content[\\/]docs[\\/](reference[\\/])?(language|property-types)[\\/]/; const GENERATED_REFERENCE_PATH = /[\\/]content[\\/]docs[\\/]generated[\\/]reference[\\/]/; +// The reference chapters the safety manual writes itself. The main +// documentation serves a much larger `reference/` that states no +// requirements, so this only applies where `referenceRequiresIds` is set. The +// manual's synced property-types pages sit below this path too, but they +// match SPEC_PATH first. +const MANUAL_REFERENCE_PATH = /[\\/]content[\\/]docs[\\/]reference[\\/]/; // A feature outside the certified subset is wrapped in the `` // component: the safety manual renders nothing for it, but rehype runs before @@ -80,7 +90,7 @@ function textPreview(node) { } export default function rehypeSlsIds({ - generatedReferenceRequiresIds = false, + referenceRequiresIds = false, renderBadge = true, } = {}) { // Closure-scoped: persists across files in one build, so a duplicate @@ -100,28 +110,35 @@ export default function rehypeSlsIds({ const isOwnPage = !siteRoot || sourcePath.startsWith(siteRoot); const isSpec = isOwnPage && SPEC_PATH.test(sourcePath); // Both sites generate the reference from the same doc comments, but - // only the safety manual treats it as normative. - const isNormativeReference = - generatedReferenceRequiresIds && - isOwnPage && - !isSpec && - GENERATED_REFERENCE_PATH.test(sourcePath); + // only the safety manual treats it, and the reference chapters it + // writes itself, as normative. + const isReference = referenceRequiresIds && isOwnPage && !isSpec; + const isGeneratedReference = + isReference && GENERATED_REFERENCE_PATH.test(sourcePath); + const isManualReference = + isReference && MANUAL_REFERENCE_PATH.test(sourcePath); const frontmatter = file?.data?.astro?.frontmatter; - // A `notInSC: true` chapter is outside the safety corpus, so it - // carries no identifiers and its markers are dropped. - const notInSC = isSpec && Boolean(frontmatter?.notInSC); - const assignsIds = (isSpec && !notInSC) || isNormativeReference; + // A chapter outside the SC subset, and a navigational page like a + // section landing page, both state no requirements: they carry no + // identifiers and their markers are dropped. + const statesNoRequirements = + (isSpec && Boolean(frontmatter?.notInSC)) || + frontmatter?.normative === false; + const assignsIds = + (isSpec || isGeneratedReference || isManualReference) && + !statesNoRequirements; // Draft pages aren't published, so they need no ids. const requireIds = assignsIds && !frontmatter?.draft; - // In the language specification, only top-level paragraphs are - // normative: nested ones are asides and list items. The generated SC - // reference and the property-types reference are normative at any depth - // -- the latter wraps normative prose in components like - // , so an untagged nested paragraph there is a mistake - // and fails the build rather than slipping through. + // In the language specification and the manual's own reference + // chapters, only top-level paragraphs are normative: nested ones are + // asides and list items. The generated SC reference and the + // property-types reference are normative at any depth -- both wrap + // normative prose in components like , so an untagged + // nested paragraph there is a mistake and fails the build rather than + // slipping through. const isPropertyTypes = isSpec && /[\\/]property-types[\\/]/.test(sourcePath); - const requiredAtAnyDepth = isNormativeReference || isPropertyTypes; + const requiredAtAnyDepth = isGeneratedReference || isPropertyTypes; // Tracks ids claimed during *this* invocation, so re-processing the // same file (dev-mode hot reload) re-claims its own ids cleanly while diff --git a/docs/common/src/utils/remark-base-links.mjs b/docs/common/src/utils/remark-base-links.mjs new file mode 100644 index 00000000000..0e0a8dd4350 --- /dev/null +++ b/docs/common/src/utils/remark-base-links.mjs @@ -0,0 +1,41 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +// Prefix the site base to the internal links of a page. Astro doesn't apply +// `base` to markdown links, so a site served under a path would need every +// link to spell that path out. Sources instead write links from the site root +// -- `/language/properties/` -- and this adds the base, in one place, at build +// time. That keeps the sources free of the deployment layout, which the +// release job rewrites per version. +// +// It matters for more than the output: starlight-links-validator resolves a +// link against the deployed path, so an unprefixed link fails validation as a +// link to an unknown page. Remark runs before the validator's rehype pass, so +// what gets checked is the prefixed link. + +/** Rewrite the `url` of every link and link definition in the tree. */ +function walk(node, fn) { + if (node.type === "link" || node.type === "definition") { + fn(node); + } + for (const child of node.children ?? []) { + walk(child, fn); + } +} + +export default function remarkBaseLinks({ base = "/" } = {}) { + const prefix = base.replace(/\/+$/, ""); + // Served at the root, so the links already read as they should. + if (prefix === "") { + return () => {}; + } + return (tree) => { + walk(tree, (node) => { + // A protocol-relative URL also starts with a slash and leaves the + // site, so it isn't ours to rewrite. + if (node.url?.startsWith("/") && !node.url.startsWith("//")) { + node.url = `${prefix}${node.url}`; + } + }); + }; +} diff --git a/docs/safety/astro.config.mjs b/docs/safety/astro.config.mjs index 56243cfaa02..a2a4af5fa5f 100644 --- a/docs/safety/astro.config.mjs +++ b/docs/safety/astro.config.mjs @@ -15,6 +15,7 @@ import { SAFETY_DOCS_BASE_PATH, } from "./src/safety-site-config.mjs"; import rehypeSlsIds from "@slint/common-files/src/utils/rehype-sls-ids.mjs"; +import remarkBaseLinks from "@slint/common-files/src/utils/remark-base-links.mjs"; const _safetyOrigin = String(SAFETY_DOCS_BASE_URL).replace(/\/+$/, ""); const _safetyAtRoot = SAFETY_DOCS_BASE_PATH === "/"; @@ -33,9 +34,10 @@ export default defineConfig({ markdown: { // Only SC-covered content reaches this site's generated reference, so // every paragraph of it carries a traceability id. + remarkPlugins: [[remarkBaseLinks, { base: _safetyBase ?? "/" }]], rehypePlugins: [ rehypeExternalLinksSlint, - [rehypeSlsIds, { generatedReferenceRequiresIds: true }], + [rehypeSlsIds, { referenceRequiresIds: true }], ], }, integrations: [ @@ -52,7 +54,7 @@ export default defineConfig({ Header: "@slint/common-files/src/components/Header.astro", Banner: "@slint/common-files/src/components/Banner.astro", }, - plugins: [slintStarlightLinksValidatorPlugin({ errorOnRelativeLinks: false })], + plugins: [slintStarlightLinksValidatorPlugin({ errorOnRelativeLinks: true })], social: slintStarlightSocial, sidebar: [ { label: "Slint SC Safety Manual", slug: "index" }, diff --git a/docs/safety/scripts/sync-language-spec.mjs b/docs/safety/scripts/sync-language-spec.mjs index 94010444e42..cc36ae441ef 100644 --- a/docs/safety/scripts/sync-language-spec.mjs +++ b/docs/safety/scripts/sync-language-spec.mjs @@ -9,7 +9,10 @@ // // The chapters use relative links so that they resolve in both sites. Links // that point outside the specification differ per site and are rewritten via -// LINK_MAP below. +// LINK_MAP below. The copies written here then get every link rewritten from +// the site base, because starlight-links-validator skips relative links +// entirely: a relative link would go unchecked and could rot into a dead one +// unnoticed. import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -27,11 +30,32 @@ function isNotInSC(content) { // Drop `` blocks entirely. The manual omits them at runtime anyway, // but MDX evaluates a component's children eagerly, so a throwing component // left inside one (e.g. CodeSnippetMD referencing a main-docs-only image) -// would still break the build here. Removing them leaves only the SC content. +// would still break the build here. Removing them leaves only the SC content, +// and with it only links this site can resolve: a block outside the subset +// links to chapters the manual doesn't serve, which link validation would +// reject once the links are written from the site base. function stripNotInSC(content) { return content.replace(/[\s\S]*?<\/NotInSC>\n?/g, ""); } +// Rewrite the markdown links of a page served at `pageUrl` from the site root, +// so that starlight-links-validator checks them: it skips relative links +// rather than resolving them. Resolving against the page's own URL keeps the +// sources readable, since they stay relative. remark-base-links.mjs adds the +// base at build time. +function linksFromRoot(content, pageUrl) { + return content.replace(/\]\((\.\.?\/[^)]*)\)/g, (_, url) => { + const resolved = new URL(url, `https://slint.dev${pageUrl}`); + return `](${resolved.pathname}${resolved.hash})`; + }); +} + +// URL of a synced page below `sectionUrl`; an index page heads its section. +function pageUrl(sectionUrl, entry) { + const stem = entry.replace(/\.mdx?$/, ""); + return stem === "index" ? sectionUrl : `${sectionUrl}${stem}/`; +} + // Copy the files under `sourceDir` accepted by `accept` into `targetDir`, // optionally transforming each file, writing only changed files and removing // stale ones (minimal watcher churn). @@ -47,7 +71,7 @@ function syncDir(sourceDir, targetDir, accept, transform = (c) => c) { continue; } wanted.add(entry); - const out = transform(content); + const out = transform(content, entry); const targetFile = join(targetDir, entry); if (!existsSync(targetFile) || readFileSync(targetFile, "utf-8") !== out) { writeFileSync(targetFile, out); @@ -82,6 +106,7 @@ for (const entry of readdirSync(source)) { for (const [from, to] of LINK_MAP) { content = content.replaceAll(from, to); } + content = linksFromRoot(stripNotInSC(content), pageUrl("/language/", entry)); const targetFile = join(target, entry); if (!existsSync(targetFile) || readFileSync(targetFile, "utf-8") !== content) { writeFileSync(targetFile, content); @@ -101,7 +126,8 @@ syncDir( join(here, "../../astro/src/content/docs/reference/property-types"), join(here, "../src/content/docs/reference/property-types"), (content) => !isNotInSC(content), - stripNotInSC, + (content, entry) => + linksFromRoot(stripNotInSC(content), pageUrl("/reference/property-types/", entry)), ); console.log(`Synced language specification from ${source}`); diff --git a/docs/safety/src/content.config.ts b/docs/safety/src/content.config.ts index f078c072210..ac901d9ba53 100644 --- a/docs/safety/src/content.config.ts +++ b/docs/safety/src/content.config.ts @@ -1,9 +1,21 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT -import { defineCollection } from "astro:content"; +import { defineCollection, z } from "astro:content"; import { docsLoader } from "@astrojs/starlight/loaders"; import { docsSchema } from "@astrojs/starlight/schema"; export const collections = { - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), + docs: defineCollection({ + loader: docsLoader(), + schema: docsSchema({ + extend: z.object({ + // Navigational page, like a section landing page: it states + // no requirements, so its paragraphs carry no `{#sls.…}` + // identifiers. Defaults to true for every other page under + // `reference/`, which the completeness check in + // rehype-sls-ids.mjs holds to the corpus rules. + normative: z.boolean().optional(), + }), + }), + }), }; diff --git a/docs/safety/src/content/docs/development-phases.mdx b/docs/safety/src/content/docs/development-phases.mdx index fc20dd30342..0082913dbe5 100644 --- a/docs/safety/src/content/docs/development-phases.mdx +++ b/docs/safety/src/content/docs/development-phases.mdx @@ -41,7 +41,7 @@ For external contributions, the reviewer must merge the PR. We have a set of tests that are executed as part of this phase. These tests can be run locally using the "cargo test" command. -They are described in more detail here: [Tests](../qualification-plan/test-cases/). +They are described in more detail here: [Tests](/qualification-plan/test-cases/). diff --git a/docs/safety/src/content/docs/reference/generated-code.mdx b/docs/safety/src/content/docs/reference/generated-code.mdx index 67b19618fac..bbc58ecd32b 100644 --- a/docs/safety/src/content/docs/reference/generated-code.mdx +++ b/docs/safety/src/content/docs/reference/generated-code.mdx @@ -4,7 +4,7 @@ description: The Rust API that the Slint SC compiler generates. --- The Slint SC compiler translates a `.slint` file into Rust code that depends -only on the `slint-sc` runtime crate. +only on the `slint-sc` runtime crate. \{#sls.gen.output} ## The Component Struct @@ -28,7 +28,7 @@ pub fn render_rgb8(&self, width: u32, height: u32, frame_buffer: &mut [u8]) -> R ``` Renders the window into `frame_buffer` as described in -[Rendering](../rendering/): +[Rendering](/reference/rendering/): `width * height` pixels in row-major order, each pixel three bytes — red, green, blue. \{#sls.gen.render-rgb8} @@ -44,7 +44,7 @@ returns `Err(RenderError::InvalidFrameBufferSize)` and paints nothing. ## Properties The struct holds the value of each -[property declared](../../language/properties/#sls.prop.decl.form) on the +[property declared](/language/properties/#sls.prop.decl.form) on the root element in a private field. \{#sls.gen.prop.field} Kebab-case property names become snake_case in the generated names: diff --git a/docs/safety/src/content/docs/reference/index.mdx b/docs/safety/src/content/docs/reference/index.mdx index 6f9b86e7d40..103eb811606 100644 --- a/docs/safety/src/content/docs/reference/index.mdx +++ b/docs/safety/src/content/docs/reference/index.mdx @@ -2,6 +2,7 @@ title: API Reference description: Reference for the Slint SC certified subset of the .slint language. slug: reference +normative: false --- This section documents the elements, properties, callbacks, functions, structs diff --git a/docs/safety/src/content/docs/reference/rendering.mdx b/docs/safety/src/content/docs/reference/rendering.mdx index 46221fef5e6..d3c6ad7a5f0 100644 --- a/docs/safety/src/content/docs/reference/rendering.mdx +++ b/docs/safety/src/content/docs/reference/rendering.mdx @@ -4,7 +4,7 @@ description: How a window is rendered into a frame buffer. --- The application renders by calling the `render_rgb8` function of the -[generated code](../generated-code/). +[generated code](/reference/generated-code/). \{#sls.paint.invocation} ## Model diff --git a/docs/safety/src/content/docs/requirements/index.mdx b/docs/safety/src/content/docs/requirements/index.mdx index 1b4a18e84e2..c42b5f92b2a 100644 --- a/docs/safety/src/content/docs/requirements/index.mdx +++ b/docs/safety/src/content/docs/requirements/index.mdx @@ -24,14 +24,14 @@ Each Requirement under this Section has a descriptive ID that begins with SR_, a All ISO26262 references below are valid for the 2018 edition of the standard. -* ISO 26262-4 5.x: See [Development Phases](../development-phases/). -* ISO 26262-4 9.x: See [Validation](../qualification-plan/validation/). -* ISO 26262-6 7.x: See [Architecture Design](../using-slint-sc/#slint-sc-architecture-design-iso-262626-74) -* ISO 26262-8 5.x: See [Distributed Development](../development-process/#distributed-development-iso-26262-8-5x) +* ISO 26262-4 5.x: See [Development Phases](/development-phases/). +* ISO 26262-4 9.x: See [Validation](/qualification-plan/validation/). +* ISO 26262-6 7.x: See [Architecture Design](/using-slint-sc/#slint-sc-architecture-design-iso-262626-74) +* ISO 26262-8 5.x: See [Distributed Development](/development-process/#distributed-development-iso-26262-8-5x) * ISO 26262-8 6.4: The safety requirements shall be traceable to the safety goals and to the safety concept. The traceability shall be documented and maintained. -* ISO 26262-8 7.x: See [Configuration Management](../development-process/#configuration-management-iso-26262-8-7x) -* ISO 26262-8 8.x: See [Change Management](../development-process/#change-management-iso-26262-8-8x) -* ISO 26262-8 9.4.x: See [Verification](../development-process/#verification-iso-26262-8-94x) -* ISO 26262-8 11.4.8: See [The Development Process](../development-process/#the-development-process-iso-26262-8-1148) -* ISO 26262-8 12.x: See [Software Component Qualification](../development-process/#software-component-qualification-iso-26262-8-12x) +* ISO 26262-8 7.x: See [Configuration Management](/development-process/#configuration-management-iso-26262-8-7x) +* ISO 26262-8 8.x: See [Change Management](/development-process/#change-management-iso-26262-8-8x) +* ISO 26262-8 9.4.x: See [Verification](/development-process/#verification-iso-26262-8-94x) +* ISO 26262-8 11.4.8: See [The Development Process](/development-process/#the-development-process-iso-26262-8-1148) +* ISO 26262-8 12.x: See [Software Component Qualification](/development-process/#software-component-qualification-iso-26262-8-12x) diff --git a/docs/slint-doc-generator/traceability.rs b/docs/slint-doc-generator/traceability.rs index c69d9ab116b..84bf3e53755 100644 --- a/docs/slint-doc-generator/traceability.rs +++ b/docs/slint-doc-generator/traceability.rs @@ -60,8 +60,10 @@ struct SpecPage { file: String, /// From the frontmatter. title: String, - /// Site-relative URL of the page, for linking its anchors from the matrix. - /// The matrix sits two levels deep, so it starts with `../../`. + /// URL of the page from the site root, for linking its anchors from the + /// matrix. Not relative, because starlight-links-validator skips relative + /// links: one here would go unchecked and could rot into a dead anchor. + /// remark-base-links.mjs adds the site base at build time. base: String, /// The specification index heads its section; every other page nests /// under one. @@ -73,6 +75,11 @@ struct SpecPage { /// Covers the full language only: the safety manual leaves the chapter /// out, so its anchors, if any, aren't part of the traceability corpus. not_in_sc: bool, + /// States requirements, the default. A navigational page like a section + /// landing page sets `normative: false`; rehype-sls-ids.mjs drops its + /// markers rather than turning them into anchors, so citing one here + /// would dead-link. + normative: bool, } struct TestRef { @@ -190,6 +197,7 @@ fn parse_spec_page(file: &str, text: &str) -> (SpecPage, Option) { anchors: Vec::new(), draft: false, not_in_sc: false, + normative: true, }; let mut in_comment = false; let mut in_fence = false; @@ -210,6 +218,8 @@ fn parse_spec_page(file: &str, text: &str) -> (SpecPage, Option) { page.draft = true; } else if t == "notInSC: true" { page.not_in_sc = true; + } else if t == "normative: false" { + page.normative = false; } continue; } @@ -283,11 +293,8 @@ fn scan_spec_pages(dir: &Path) -> Result, Box Result, Box Result, Box