84 lines
3.1 KiB
QML
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, "")
|
|
}
|
|
}
|
|
}
|