Files

84 lines
3.1 KiB
QML

// The manual's table of contents, with the titles read from the manual itself.
//
// Chapter files are numbered so their order is their filename, and each one
// begins with its own title as a first-level heading. Reading that heading from
// the file rather than repeating it in QML means a chapter cannot be renamed in
// one place and not the other -- which is what had already happened by the time
// anybody compared the two. There is deliberately no `label:` beside a filename
// below; if one appears, the drift is back.
//
// Two pages need this list: the reader, and About's Manual card. Neither owns
// it, so it lives here and both instantiate it. This is the one place the
// chapter files are named.
//
// The chapters live beside the shell in manual/, reached through
// Quickshell.shellDir: that resolves whether or not the repository is where it
// usually is, and a path walked upward out of the shell directory does not.
import Quickshell
import Quickshell.Io
import QtQml
import QtQuick
Item {
id: root
// Display order is filename order.
readonly property var chapters: [
{ file: "01-coming-from-another-desktop.md" },
{ file: "02-the-keyboard.md" },
{ file: "03-windows-and-workspaces.md" },
{ file: "04-when-something-breaks.md" },
{ file: "05-making-it-yours.md" }
]
// file -> first heading, filled in as each file is read.
property var headings: ({})
// [{ file, title }]. A chapter whose file has not been read yet -- or
// cannot be -- falls back to a title made out of its filename, so the list
// is never blank and never a row of paths.
readonly property var titled: root.chapters.map(chapter => ({
file: chapter.file,
title: String(root.headings[chapter.file] ?? "") !== ""
? root.headings[chapter.file]
: root.titleFromFilename(chapter.file)
}))
function titleFromFilename(file: string): string {
const stem = file.replace(/^\d+-/, "").replace(/\.md$/, "").replace(/-/g, " ");
return stem.charAt(0).toUpperCase() + stem.slice(1);
}
// The first level-one heading, which every chapter opens with.
function headingOf(text: string): string {
for (const line of String(text ?? "").split("\n")) {
const match = /^#\s+(.+?)\s*$/.exec(line);
if (match)
return match[1];
}
return "";
}
// Reassigned rather than mutated: a property change on a plain object does
// not fire, and `titled` would keep answering with the old headings.
function absorb(file: string, text: string): void {
const next = Object.assign({}, root.headings);
next[file] = root.headingOf(text);
root.headings = next;
}
Instantiator {
model: root.chapters
delegate: FileView {
required property var modelData
path: Quickshell.shellDir + "/manual/" + modelData.file
printErrors: false
onLoaded: root.absorb(modelData.file, this.text())
onLoadFailed: root.absorb(modelData.file, "")
}
}
}