From 4a72463e81c6db190c0b76f801f983024ae9221a Mon Sep 17 00:00:00 2001 From: burinc Date: Mon, 7 Sep 2026 14:31:12 +1000 Subject: [PATCH] feat: every project's site gets a default Contributing page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit raylib-android's own docs got a hand-written CONTRIBUTING.md and a mirrored docs/guide/contributing.md this session, and it became obvious partway through that "how to open an issue against a jlt-commons project" is the same answer for every project using this engine, so it belongs here, on the same footing as base.html or 404.html, rather than copied into each project's own docs/guide/. - resources/content/contributing.md: the default page content (the Clojure community etiquette guide, adapted, plus a short invitation). - src/site/core.clj: - discover-doc-ids now adds the engine's default-root-doc-basenames (currently just "contributing.md") to a project's guide-dir root group, for any name the project has not written itself. Nested subdirectories are untouched. - new doc-source resolves a doc-id to the project's own file if it exists, else the engine's bundled resources/content/ — the override mechanism. render-all-docs now goes through it instead of a raw slurp. - test/site/core_test.clj: three new tests — a project with no contributing.md gets the engine default, it lands in nav right after index.md (a sort-order check, not just presence), and a project's own docs/guide/contributing.md replaces the default outright. - README.md: new "The default Contributing page" section documenting the mechanism and its override, plus stating the general shape for any future default page. Verified against a real project, not just the fixture suite: built raylib-android against this checkout and confirmed guide/contributing.html renders, appears in nav, and raylib-android's own docs/check-site.sh still passes (33 images, 4 guide pages, up from 2). Also ran the CI workflow's own end-to-end demo-project build by hand, including its base-path escape check, since this change touches the URLs every project's pages emit. bb test: 70 tests, 153 assertions, 0 failures, 0 errors. --- README.md | 22 ++++++++ resources/content/contributing.md | 57 +++++++++++++++++++++ src/site/core.clj | 84 ++++++++++++++++++++++++------- test/site/core_test.clj | 47 ++++++++++++++--- 4 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 resources/content/contributing.md diff --git a/README.md b/README.md index 869b554..a7f854a 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,28 @@ A project template overrides any engine template of the same name, so a bespoke inheriting none of the engine's later fixes to it, so prefer a block override where one will do. +## The default Contributing page + +Every project's guide gets a `guide/contributing.html`, in the nav right +after `index.md`, even without writing one. It comes from the engine's own +`resources/content/contributing.md`, on the same footing as `base.html` or +`404.html`: shared chrome, not a project's content, which is the point — +"how to open an issue against a jlt-commons project" is the same answer +everywhere, so it lives in one place instead of N copies that drift. + +A project with something project-specific to say (a note on known-broken +areas, its own escalation path, anything beyond the shared etiquette) +writes its own `docs/guide/contributing.md`. That file is discovered the +same way any other guide page is, and having one there replaces the +engine's default outright rather than merging with it. + +This is the general shape for any future default page: the engine's +`discover-doc-ids` adds its own basenames to a project's guide-dir root +group (never a nested subdirectory) for names the project has not written +itself, and `doc-source` resolves each one to the project's file if it +exists, else the engine's bundled default. `contributing.md` is the only +one today. + ## Markdown Standard markdown through `markdown-clj`, plus: diff --git a/resources/content/contributing.md b/resources/content/contributing.md new file mode 100644 index 0000000..031fc38 --- /dev/null +++ b/resources/content/contributing.md @@ -0,0 +1,57 @@ +# Contributing + +This project is built by the community and the team behind it, and +suggestions for improvement are welcome. If something is missing, unclear, +or just plain wrong, open an issue for a question or a gap, and a pull +request for anything you have already worked out. + +## Etiquette + +Adapted from the Clojure community's own +[etiquette guide](https://clojure.org/community/etiquette), since it says +what we would want to say ourselves and says it well. + +Issues, pull requests and discussion here are for people who make things. +Most messages should have one of these forms: + +- I made something. Here is my contribution. +- I am trying to use this and having trouble, please help. +- I can help you with that. +- I am trying to build something on top of this and having trouble, please + help. +- I can help you build something. + +They are not the place for opinion pieces or diatribes, and not the place +for advocacy about what "ought" to be built. If you think something ought +to exist, the fastest way to find out is to build it and send a pull +request. Otherwise, respect that other people get to choose what they do +with their time. + +Disagreements about how something has been, or will be, done should take +the form of technical arguments. A technical argument that gets, and +gives, respect: + +- Keeps it short. +- Sticks to the facts. +- Uses logic. +- Leaves people out of it. +- Avoids rhetorical devices: superfluous or opinion-laden adjectives, + claims to speak for the community or that everyone agrees with you, + threats of what will happen unless things go your way, any flavour of + "the sky is falling." + +If you are not the one doing the work, restrict your input to short +technical arguments supporting your position. If someone has already made +your point, a "+1" is enough, and keeping posts short is worth doing on +its own. + +Ignoring these guidelines costs the time of the people who make things, +which is worth caring about if you intend to be one of them. + +--- + +This is this project's default Contributing page, shipped by +[docs-engine](https://github.com/jlt-commons/docs-engine) so every +jlt-commons project's site carries the same one without copying it around. +A project with something project-specific to add writes its own +`docs/guide/contributing.md`, which replaces this page entirely. diff --git a/src/site/core.clj b/src/site/core.clj index e376a45..c2d7560 100644 --- a/src/site/core.clj +++ b/src/site/core.clj @@ -75,27 +75,47 @@ (into ["index.md"] (remove #{"index.md"} sorted)) (vec sorted)))) +(def ^:private default-root-doc-basenames + "Basenames the engine supplies for the guide's root directory even when + the project has not written one itself — the same relationship + base.html and 404.html already have to a project (shared chrome, not + project content), applied to a page instead of a template. A + project's own file of the same name always wins; see doc-source. Only + the root directory gets defaults, not a nested docs/guide/ subtree. + + Currently just \"contributing.md\": every jlt-commons project's site + gets a Contributing page without copying its content into every + project, and a project with something project-specific to add writes + docs/guide/contributing.md like any other page, which replaces this + one entirely." + #{"contributing.md"}) + (defn discover-doc-ids "Auto-discovered nav order: every *.md anywhere under guide-dir (any - depth), as POSIX-style paths relative to guide-dir. Ordered - directory-by-directory so it reads top-to-bottom like the guide's - intended sequence: directories + depth), as POSIX-style paths relative to guide-dir, PLUS the engine's + own default-root-doc-basenames for any of those the project has not + written itself. Ordered directory-by-directory so it reads top-to- + bottom like the guide's intended sequence: directories themselves in sorted order (guide-dir's own root first, since '' sorts before any name), each directory's own files pinning ITS index.md first if present, else sorted. Used when a project has no projects//docpages.edn curated-order override. + A doc-id this returns does not always name a file guide-dir actually + has — see doc-source, which resolves each one to either the project's + own file or the engine's bundled default. + For a flat guide-dir (the shape every project had before one introduced nested subdirectories under docs/guide/) this reduces to - exactly the previous flat behavior: one directory group (the root), - its files sorted with index.md pinned first. Plain alphabetical sort - on full relative paths would get a nested project's per-directory - index.md wrong when numbered siblings start above 01 (verified - against a sample nested project's section-three/, whose files are - index.md, 02-getting-started.md, 03-closing.md — alphabetical-on-full- - path would order index.md LAST, after 02/03, since digits sort below - the letter 'i'); grouping by directory and pinning per-group avoids - that." + exactly the previous flat behavior plus the defaults: one directory + group (the root), its files sorted with index.md pinned first. Plain + alphabetical sort on full relative paths would get a nested project's + per-directory index.md wrong when numbered siblings start above 01 + (verified against a sample nested project's section-three/, whose + files are index.md, 02-getting-started.md, 03-closing.md — + alphabetical-on-full-path would order index.md LAST, after 02/03, + since digits sort below the letter 'i'); grouping by directory and + pinning per-group avoids that." [guide-dir] (let [root (.toPath (io/file guide-dir)) all-rel (->> (file-seq (io/file guide-dir)) @@ -104,12 +124,36 @@ (map (fn [rel] (str/replace rel java.io.File/separator "/")))) dir-of (fn [rel] (let [i (str/last-index-of rel "/")] (if i (subs rel 0 i) ""))) base-of (fn [rel] (let [i (str/last-index-of rel "/")] (if i (subs rel (inc i)) rel))) - by-dir (group-by dir-of all-rel)] + by-dir (group-by dir-of all-rel) + ;; The defaults join the root group only, and only the ones the + ;; project has not already written under that name — the + ;; override. For the root group a relative path IS its basename, + ;; so adding basenames straight into by-dir's "" entry is safe. + by-dir (update by-dir "" + (fn [root-rels] + (into (vec root-rels) + (remove (set root-rels) default-root-doc-basenames))))] (vec (mapcat (fn [dir] (let [bases (pin-index-first (map base-of (get by-dir dir)))] (map (fn [base] (if (= dir "") base (str dir "/" base))) bases))) (sort (keys by-dir)))))) +(defn doc-source + "doc-id's markdown text: the project's own guide-dir/doc-id if it + exists, else the engine's own bundled default for that name (see + default-root-doc-basenames) — the override mechanism. Throws if + neither exists, which discover-doc-ids should make impossible for any + doc-id it actually returned." + [guide-dir doc-id] + (let [project-file (io/file guide-dir doc-id)] + (if (fs/exists? project-file) + (slurp project-file) + (if-let [res (io/resource (str "content/" doc-id))] + (slurp res) + (throw (ex-info (str "no source for doc-id " doc-id + " — neither the project nor the engine has it") + {:guide-dir (str guide-dir) :doc-id doc-id})))))) + (def ^:private mermaid-marker "What site.markdown/mermaidify emits, and what a project writes by hand in its own template when it wants a diagram outside a markdown fence." @@ -136,15 +180,17 @@ (defn render-all-docs "doc-id -> {:title :toc-html :body-html :slug}, for every doc-id. - Always renders via md/rewrite-nested-doc-links (built per doc-id, - from its own path relative to guide-dir) rather than defaulting to - plain rewrite-doc-links — the nested-aware rewriter produces - byte-identical output to the flat one for every flat (no-'/') doc-id, - so this is safe for every existing project, not just nested ones." + Source text comes from doc-source, so a doc-id the project never wrote + itself still renders from the engine's own default. Always renders via + md/rewrite-nested-doc-links (built per doc-id, from its own path + relative to guide-dir) rather than defaulting to plain + rewrite-doc-links — the nested-aware rewriter produces byte-identical + output to the flat one for every flat (no-'/') doc-id, so this is safe + for every existing project, not just nested ones." [guide-dir doc-ids] (into {} (for [doc-id doc-ids] - (let [raw (slurp (io/file guide-dir doc-id)) + (let [raw (doc-source guide-dir doc-id) {:keys [title toc-html body-html]} (md/render-doc-page raw (md/rewrite-nested-doc-links doc-id))] [doc-id {:title title :toc-html toc-html :body-html body-html diff --git a/test/site/core_test.clj b/test/site/core_test.clj index c5abef1..ff29edc 100644 --- a/test/site/core_test.clj +++ b/test/site/core_test.clj @@ -68,12 +68,12 @@ (fs/create-dirs (io/file docs "media")) (spit (io/file docs "media" "x.gif") "GIF89a")) (let [site (cond-> {:title "jlt-commons" :description "d" :github-url "https://example.invalid" - :base-path base - :guide-dir guide - :templates-dir templates - :output-dir (io/file (str tmp) "_site") - :home-template (when home-template? "home.html") - :asset-dirs (when assets? [(io/file docs "media")])} + :base-path base + :guide-dir guide + :templates-dir templates + :output-dir (io/file (str tmp) "_site") + :home-template (when home-template? "home.html") + :asset-dirs (when assets? [(io/file docs "media")])} (some? mermaid-override) (assoc :mermaid mermaid-override))] (core/generate! site) {:out (:output-dir site) @@ -209,3 +209,38 @@ (is (false? (core/mermaid-needed? {} nil))) (is (true? (core/mermaid-needed? {:mermaid true} "no diagram here"))) (is (false? (core/mermaid-needed? {:mermaid false} "
x
")))) + +;; A project that has never written its own Contributing page still gets +;; the engine's default one, so nothing about "how to engage with this +;; project" needs copying into every project that uses this engine. + +(deftest a-project-with-no-contributing-page-gets-the-engine-default + (let [{:keys [out]} (build-fixture-site! "/some-lib") + contributing (slurp (io/file out "guide" "contributing.html"))] + (is (str/includes? contributing "Etiquette")) + (is (str/includes? contributing "docs-engine")))) + +(deftest the-default-contributing-page-is-in-nav-right-after-index + ;; index.md is pinned first; among what is left, "contributing.md" sorts + ;; alphabetically ahead of "plain.md", so this is also a sort-order check, + ;; not just a presence check. + (let [{:keys [doc]} (build-fixture-site! "/some-lib")] + (is (str/includes? doc "href=\"/some-lib/guide/contributing.html\"")) + (let [index-at (str/index-of doc "/guide/index.html") + contrib-at (str/index-of doc "/guide/contributing.html") + plain-at (str/index-of doc "/guide/plain.html")] + (is (< index-at contrib-at plain-at))))) + +(deftest a-project-with-its-own-contributing-page-overrides-the-default + (let [tmp (fs/create-temp-dir {:prefix "jltc-site"}) + docs (io/file (str tmp) "docs") + guide (io/file docs "guide")] + (fs/create-dirs guide) + (spit (io/file guide "index.md") "# Intro\n") + (spit (io/file guide "contributing.md") "# Contributing\n\nOur own house rules.\n") + (let [site {:title "t" :description "d" :base-path "" :guide-dir guide + :output-dir (io/file (str tmp) "_site")}] + (core/generate! site) + (let [contributing (slurp (io/file (:output-dir site) "guide" "contributing.html"))] + (is (str/includes? contributing "Our own house rules.")) + (is (not (str/includes? contributing "Etiquette")))))))