version docs pages

This commit is contained in:
outfoxxed 2025-07-22 01:08:30 -07:00
parent 5865251560
commit f79392054b
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
31 changed files with 329 additions and 388 deletions

View file

@ -6,8 +6,7 @@
yarn-berry, yarn-berry,
nodejs, nodejs,
cacert, cacert,
quickshell-types ? null, versions,
masterBranch ? false,
}: stdenv.mkDerivation (final: let }: stdenv.mkDerivation (final: let
nodeModules = stdenv.mkDerivation { nodeModules = stdenv.mkDerivation {
pname = "${final.pname}-node_modules"; pname = "${final.pname}-node_modules";
@ -70,8 +69,7 @@ in {
''; '';
PRODUCTION = true; PRODUCTION = true;
MASTER_BRANCH = masterBranch; VERSION_FILE_PATH = versions;
SECRET_MODULES_PATH = if quickshell-types == null then "" else quickshell-types;
buildPhase = '' buildPhase = ''
HOME=$(pwd)/garbage-tooling yarn build HOME=$(pwd)/garbage-tooling yarn build

51
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1730785428, "lastModified": 1752950548,
"narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", "narHash": "sha256-NS6BLD0lxOrnCiEOcvQCDVPXafX1/ek1dfJHX1nUIzc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", "rev": "c87b95e25065c028d31a94f06a62927d18763fdf",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -15,52 +15,9 @@
"type": "indirect" "type": "indirect"
} }
}, },
"quickshell": {
"inputs": {
"nixpkgs": [
"quickshell-docs",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747564942,
"narHash": "sha256-XBs7dhqsY3vdrtukt9Qwcqz8CSixMnm/XvpqD+kn+TA=",
"ref": "refs/heads/master",
"rev": "823456b58e83880e8f6bfaa785ac949975817d2a",
"revCount": 529,
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
},
"original": {
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell"
}
},
"quickshell-docs": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"quickshell": "quickshell"
},
"locked": {
"lastModified": 1747388043,
"narHash": "sha256-/iJ+Q2Z4rvwWCMHhUXeuGD86NqYLwEVrA+WRlIPTJnA=",
"ref": "refs/heads/master",
"rev": "82a47eae2403c54909cf0249deaddbbba29ba661",
"revCount": 100,
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell-docs"
},
"original": {
"type": "git",
"url": "https://git.outfoxxed.me/quickshell/quickshell-docs"
}
},
"root": { "root": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs"
"quickshell-docs": "quickshell-docs"
} }
} }
}, },

View file

@ -1,23 +1,15 @@
{ {
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable"; nixpkgs.url = "nixpkgs/nixos-unstable";
quickshell-docs = {
url = "git+https://git.outfoxxed.me/quickshell/quickshell-docs";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = { self, nixpkgs, quickshell-docs }: let outputs = { self, nixpkgs }: let
forEachSystem = fn: nixpkgs.lib.genAttrs forEachSystem = fn: nixpkgs.lib.genAttrs
[ "x86_64-linux" "aarch64-linux" ] [ "x86_64-linux" "aarch64-linux" ]
(system: fn system nixpkgs.legacyPackages.${system}); (system: fn system nixpkgs.legacyPackages.${system});
in { in {
packages = forEachSystem (system: pkgs: rec { packages = forEachSystem (system: pkgs: rec {
quickshell-web = pkgs.callPackage ./default.nix { quickshell-web = pkgs.callPackage ./default.nix {};
quickshell-types = quickshell-docs.packages.${system}.quickshell-types;
};
default = quickshell-web; default = quickshell-web;
}); });
}; };

View file

@ -3,3 +3,6 @@ redir /docs/configuration/intro* /docs/guide/introduction permanent
redir /docs/configuration/positioning* /docs/guide/size-position permanent redir /docs/configuration/positioning* /docs/guide/size-position permanent
redir /docs/configuration/qml-overview* /docs/guide/qml-language permanent redir /docs/configuration/qml-overview* /docs/guide/qml-language permanent
redir /docs/configuration* /docs/guide permanent redir /docs/configuration* /docs/guide permanent
@unversioned_docs path_regexp ^/docs/((guide|types).*)$
redir @unversioned_docs /docs/v0.1.0/{re.1}

View file

@ -1,7 +1,7 @@
--- ---
import { processMarkdown } from "@config/io/markdown"; import { processMarkdown } from "@config/io/markdown";
const codeDesktop = await processMarkdown(`\`\`\`qml const codeDesktop = await processMarkdown("N/A", `\`\`\`qml
// a standard desktop window // a standard desktop window
FloatingWindow { FloatingWindow {
Timer { Timer {
@ -21,7 +21,7 @@ FloatingWindow {
} }
\`\`\``); \`\`\``);
const codeMobile = await processMarkdown(`\`\`\`qml const codeMobile = await processMarkdown("N/A", `\`\`\`qml
// a standard desktop window // a standard desktop window
FloatingWindow { FloatingWindow {
Timer { Timer {

View file

@ -2,7 +2,6 @@
import "@pagefind/default-ui/css/ui.css"; import "@pagefind/default-ui/css/ui.css";
import magnifierIcon from "@icons/magnifier.svg?raw" import magnifierIcon from "@icons/magnifier.svg?raw"
--- ---
<site-search class="search-wrapper"> <site-search class="search-wrapper">
<button <button
data-open-modal data-open-modal
@ -52,7 +51,6 @@ import magnifierIcon from "@icons/magnifier.svg?raw"
<script> <script>
import { getQMLTypeLinkObject, getQMLTypeLink, getIconForLink } from '@src/config/io/helpers'; import { getQMLTypeLinkObject, getQMLTypeLink, getIconForLink } from '@src/config/io/helpers';
class SiteSearch extends HTMLElement { class SiteSearch extends HTMLElement {
constructor() { constructor() {
super(); super();
@ -107,7 +105,7 @@ import { getQMLTypeLinkObject, getQMLTypeLink, getIconForLink } from '@src/confi
if (match.length > 0){ if (match.length > 0){
for (const matching of match) { for (const matching of match) {
const linkObject = getQMLTypeLinkObject(matching[1]); const linkObject = getQMLTypeLinkObject(matching[1]);
const link = getQMLTypeLink(linkObject); const link = getQMLTypeLink("NOVERSION", linkObject);
const icon = linkObject.mtype ? getIconForLink(linkObject.mtype, false) : null; const icon = linkObject.mtype ? getIconForLink(linkObject.mtype, false) : null;
// for signal // for signal

View file

@ -5,15 +5,18 @@ export interface Props {
currentClass?: string; currentClass?: string;
} }
import { getModulesData } from "@config/io/generateTypeData"; import { getVersionsData } from "@config/io/generateTypeData";
import type { TreeEntry } from "./Tree.astro"; import type { TreeEntry } from "./Tree.astro";
import Tree from "./Tree.astro"; import Tree from "./Tree.astro";
import Link from "./Link.astro"; import Link from "./Link.astro";
import VersionSelector from "./VersionSelector.astro"
const modules = await getModulesData();
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
const versions = await getVersionsData();
const versionName = Astro.params.version;
const modules = versions.versions.find(version => version.name === versionName)?.modules;
const currentPath = Astro.url.pathname.split('/').filter(s => s !== "");
const guidePages = await getCollection("guide"); const guidePages = await getCollection("guide");
interface NavTree { interface NavTree {
@ -22,8 +25,6 @@ interface NavTree {
entries?: NavTree[], entries?: NavTree[],
} }
const currentPath = Astro.url.pathname.slice(1).split('/');
function mkTree(mount: string, pathIdx: number, { title, slug, entries }: NavTree): TreeEntry { function mkTree(mount: string, pathIdx: number, { title, slug, entries }: NavTree): TreeEntry {
const link = `${mount}/${slug}`; const link = `${mount}/${slug}`;
@ -48,43 +49,67 @@ function genGuideNav(base: string): NavTree[] | undefined {
return pages.length === 0 ? undefined : pages; return pages.length === 0 ? undefined : pages;
} }
const guide = mkTree("/docs", 1, { let versionedEntries;
title: "Usage Guide", let versionsTree;
slug: "guide",
entries: genGuideNav(""),
});
const types = mkTree("/docs", 1, { if (versionName) {
title: "Quickshell Types", versionedEntries = {
slug: "types", guide: mkTree(`/docs/${versionName}`, 2, {
entries: modules.map(module => ({ title: "Usage Guide",
title: module.name, slug: "guide",
slug: module.name, entries: genGuideNav(""),
entries: module.types.map(type => ({ }),
title: type.name, types: mkTree(`/docs/${versionName}`, 2, {
slug: type.name, title: "Quickshell Types",
})) slug: "types",
})) entries: modules!.map(module => ({
}); title: module.name,
slug: module.name,
entries: module.types.map(type => ({
title: type.name,
slug: type.name,
}))
}))
}),
};
versionsTree = {
title: `Switch Version (${versionName})`,
link: "#",
entries: versions.versions.map(version => ({
title: version.name,
link: `/docs/${version.name}${Astro.url.pathname.slice(6 + versionName.length)}`,
current: version.name == versionName,
})),
};
}
--- ---
<nav class="navtree"> <nav class="navtree">
<VersionSelector title="Versions" link=`${Astro.currentLocale}` current/>
<Link <Link
title="About Quickshell" title="About"
link="/about" link="/about"
current={Astro.url.pathname === "/about"} current={currentPath.length === 1 && currentPath[0] === "about"}
/>
<Tree {...guide}/>
<Tree {...types}/>
<Link
title="QtQuick Types"
link="https://doc.qt.io/qt-6/qtquick-qmlmodule.html"
showIcon={true}
/>
<Link
title="Quickshell Examples"
link="https://git.outfoxxed.me/outfoxxed/quickshell-examples"
showIcon={true}
/> />
{ versionedEntries && <Tree {...versionsTree as TreeEntry}/>}
<hr/>
{ versionedEntries && (
<Tree {...versionedEntries.guide}/>
<Tree {...versionedEntries.types}/>
<Link
title="QtQuick Types"
link="https://doc.qt.io/qt-6/qtquick-qmlmodule.html"
showIcon={true}
/>
<Link
title="Quickshell Examples"
link="https://git.outfoxxed.me/outfoxxed/quickshell-examples"
showIcon={true}
/>
)}
{ !versionedEntries && versions.versions.map(version => (
<Link
title={`Quickshell Documentation (${version.name})`}
link={`/docs/${version.name}/guide`}
/>
))}
</nav> </nav>

View file

@ -1,48 +0,0 @@
---
import fs from 'node:fs';
import path from 'node:path';
import Link from './Link.astro';
import Accordion from "@components/Accordion.astro"
interface Props {
title: string;
link: string;
current?: boolean;
}
interface VersionData {
default: string;
versions: {
name: string;
types: string;
}[]
}
const versionFilePath = import.meta.env.VERSION_FILE_PATH;
if (!versionFilePath){
throw new Error("no env var VERSION_FILE_PATH")
}
const absolutePath = path.resolve(process.cwd(), versionFilePath);
const versionData:VersionData = JSON.parse(fs.readFileSync(absolutePath, 'utf-8'));
const { title, link, current } = Astro.props;
---
<Accordion class=`nav-component version-collapsible ${current ? "nav-current" : ""}` {...(!current ? { open: "_" } : {})}>
<div slot="header">
<span class="nav-component nav-item nav-link">
versions
</span>
</div>
<div class="version-select-menu">
{versionData.versions.map((ver, _) => {
return (
<Link
link={`${Astro.url.pathname}`}
title=`${ver.name}`
>
{ver.name}
</Link>
)}
)}
</div>
</Accordion>

View file

@ -13,12 +13,13 @@ export interface Props {
} }
const { funcData } = Astro.props; const { funcData } = Astro.props;
const { version } = Astro.params;
--- ---
<ul class="typedata typefuncs"> <ul class="typedata typefuncs">
{ {
funcData.map(item => { funcData.map(item => {
const functionParams = item.params.length > 0 ? item.params.map((funcparam,index) => `${funcparam.name}${index !== item.params.length -1 ? ", ":""}`) : undefined const functionParams = item.params.length > 0 ? item.params.map((funcparam,index) => `${funcparam.name}${index !== item.params.length -1 ? ", ":""}`) : undefined
const retTypeLink = getQMLTypeLink(item.ret as unknown as QMLTypeLinkObject) const retTypeLink = getQMLTypeLink(version!, item.ret as unknown as QMLTypeLinkObject)
let genericType:string|undefined; let genericType:string|undefined;
let genericTypeLink:string|undefined; let genericTypeLink:string|undefined;
return ( return (
@ -37,7 +38,7 @@ const { funcData } = Astro.props;
<p class="typedata-params typefunc-params"> <p class="typedata-params typefunc-params">
{ {
item.params.map(param => { item.params.map(param => {
const paramTypeLink = getQMLTypeLink(param.type); const paramTypeLink = getQMLTypeLink(version!, param.type);
return ( return (
<span class="typedata-param typefunc-param"> <span class="typedata-param typefunc-param">
<Tag client:idle/> <Tag client:idle/>

View file

@ -14,6 +14,7 @@ export interface Props {
} }
const { props } = Astro.props; const { props } = Astro.props;
const { version } = Astro.params;
--- ---
<ul class="typedata typeprops"> <ul class="typedata typeprops">
{ {
@ -27,12 +28,12 @@ const { props } = Astro.props;
typeLink = "#" typeLink = "#"
linkText = `[${Object.keys(gadget).toString()}]` linkText = `[${Object.keys(gadget).toString()}]`
} else { } else {
typeLink = getQMLTypeLink(propData.type) typeLink = getQMLTypeLink(version!, propData.type)
linkText = propData.type.name || propData.type.type linkText = propData.type.name || propData.type.type
} }
if (propData.type.of) { if (propData.type.of) {
genericType = propData.type.of.name; genericType = propData.type.of.name;
genericTypeLink = getQMLTypeLink(propData.type.of) genericTypeLink = getQMLTypeLink(version!, propData.type.of)
} }
return ( return (
<li id={ name } class="typedata-root typeprop-root"> <li id={ name } class="typedata-root typeprop-root">
@ -54,7 +55,7 @@ const { props } = Astro.props;
return ( return (
<span class="typedata-param typefunc-param"> <span class="typedata-param typefunc-param">
<Tag client:idle/> <Tag client:idle/>
{key}:<span><a href=`${getQMLTypeLink(gadgetData as unknown as QMLTypeLinkObject)}`>{gadgetData.name}</a></span> {key}:<span><a href={getQMLTypeLink(version!, gadgetData as unknown as QMLTypeLinkObject)}>{gadgetData.name}</a></span>
</span> </span>
) )
}) })

View file

@ -1,4 +1,5 @@
--- ---
import { getQMLTypeLink } from "@config/io/helpers";
import type { QuickshellSignal } from "@config/io/types"; import type { QuickshellSignal } from "@config/io/types";
import { Tag } from "@icons"; import { Tag } from "@icons";
import TypeDetails from "./TypeDetails.astro"; import TypeDetails from "./TypeDetails.astro";
@ -9,6 +10,7 @@ export interface Props {
} }
const { signals } = Astro.props; const { signals } = Astro.props;
const { version } = Astro.params;
--- ---
<ul class="typedata typesignals"> <ul class="typedata typesignals">
{ {
@ -36,8 +38,7 @@ const { signals } = Astro.props;
<span class="typedata-param typesignal-param"> <span class="typedata-param typesignal-param">
<Tag client:idle/> <Tag client:idle/>
{param.name}<span class="type-datatype">:&nbsp;<a {param.name}<span class="type-datatype">:&nbsp;<a
href="" href={getQMLTypeLink(version!, param.type)}
target="_blank"
>{param.type.name}</a></span> >{param.type.name}</a></span>
</span> </span>
) )

View file

@ -6,8 +6,9 @@ export interface Props {
} }
const { markdown } = Astro.props; const { markdown } = Astro.props;
const { version } = Astro.params;
const html = markdown ? await processMarkdown(markdown) : null; const html = markdown ? await processMarkdown(version!, markdown) : null;
--- ---
<section class="typedata-details"> <section class="typedata-details">
{html ? <div class="typedata-detailsdata" set:html={html} /> : <em>No details provided</em>} {html ? <div class="typedata-detailsdata" set:html={html} /> : <em>No details provided</em>}

View file

@ -50,7 +50,7 @@ async function readVersionsData(): Promise<VersionsData> {
const versions = await Promise.all(data.versions.map(async (d: { name: string, types: any }) => ({ const versions = await Promise.all(data.versions.map(async (d: { name: string, types: any }) => ({
name: d.name, name: d.name,
modules: await readModulesData(d.types), modules: await readModulesData(d.types),
}))) })));
return { return {
versions, versions,
@ -70,5 +70,5 @@ export function getVersionsData(): Promise<VersionsData> {
export async function getModulesData(): Promise<ModuleData[]> { export async function getModulesData(): Promise<ModuleData[]> {
const versions = await getVersionsData(); const versions = await getVersionsData();
return versions.versions.find(v => v.name == versions.default)!.modules; return versions.versions.find(v => v.name === versions.default)!.modules;
} }

View file

@ -96,7 +96,7 @@ export function getQMLTypeLinkObject(unparsed: string) {
return hashMap[index](); return hashMap[index]();
} }
export function getQMLTypeLink({ export function getQMLTypeLink(version: string, {
type, type,
module, module,
name, name,
@ -107,7 +107,7 @@ export function getQMLTypeLink({
return "#unknown"; return "#unknown";
} }
const qtStart = "https://doc.qt.io/qt-6/"; const qtStart = "https://doc.qt.io/qt-6/";
const localStart = "/docs/types"; const localStart = `/docs/${version}/types`;
const hashMap = { const hashMap = {
local: () => { local: () => {

View file

@ -21,96 +21,109 @@ import {
getIconForLink, getIconForLink,
} from "./helpers.ts"; } from "./helpers.ts";
const remarkParseAtTypes: RemarkPlugin<[]> = () => { let currentVersion = "NOVERSION";
return (root: Md.Root): Md.Root => {
visit(root as Unist.Parent, (rawNode: Unist.Node) => {
if (
rawNode.type === "text" ||
(rawNode.type === "code" &&
(rawNode as Md.Code).lang === "qml")
) {
const node = rawNode as Md.Literal;
node.value = node.value.replace( const remarkParseAtTypes: RemarkPlugin<[]> = () => (root: Md.Root): Md.Root => {
/@@((?<module>([A-Z]\w*\.)*)(?<type>([A-Z]\w*))(\.(?!\s|$))?)?((?<member>[a-z]\w*)((?<function>\(\))|(?<signal>\(s\)))?)?(?=[$.,;:)\s]|$)/g, visit(root as Unist.Parent, (rawNode: Unist.Node) => {
(_full, ...args) => { if (
type Capture = { rawNode.type === "text" ||
module: string | undefined; (rawNode.type === "code" &&
type: string | undefined; (rawNode as Md.Code).lang === "qml")
member: string | undefined; ) {
function: string | undefined; const node = rawNode as Md.Literal;
signal: string | undefined;
};
const groups = args.pop() as Capture; node.value = node.value.replace(
/@@((?<module>([A-Z]\w*\.)*)(?<type>([A-Z]\w*))(\.(?!\s|$))?)?((?<member>[a-z]\w*)((?<function>\(\))|(?<signal>\(s\)))?)?(?=[$.,;:)\s]|$)/g,
(_full, ...args) => {
type Capture = {
module: string | undefined;
type: string | undefined;
member: string | undefined;
function: string | undefined;
signal: string | undefined;
};
if (groups.module) { const groups = args.pop() as Capture;
groups.module = groups.module.substring(
0,
groups.module.length - 1
);
const isQs = groups.module.startsWith("Quickshell");
groups.module = `99M${isQs ? "QS" : "QT_qml"}_${groups.module.replace(".", "_")}`;
} else groups.module = ""; // WARNING: rehype parser can't currently handle intra-module links
groups.type = groups.type ? `99N${groups.type}` : ""; if (groups.module) {
groups.member = groups.member groups.module = groups.module.substring(
? `99V${groups.member}` 0,
: ""; groups.module.length - 1
const type = groups.member );
? `99T${groups.function ? "func" : groups.signal ? "signal" : "prop"}` const isQs = groups.module.startsWith("Quickshell");
: ""; groups.module = `99M${isQs ? "QS" : "QT_qml"}_${groups.module.replace(".", "_")}`;
return `TYPE${groups.module}${groups.type}${groups.member}${type}99TYPE`; } else groups.module = ""; // WARNING: rehype parser can't currently handle intra-module links
}
); groups.type = groups.type ? `99N${groups.type}` : "";
} groups.member = groups.member
}); ? `99V${groups.member}`
return root; : "";
}; const type = groups.member
? `99T${groups.function ? "func" : groups.signal ? "signal" : "prop"}`
: "";
return `TYPE${groups.module}${groups.type}${groups.member}${type}99TYPE`;
}
);
}
});
return root;
}; };
const rehypeRewriteTypelinks: RehypePlugin<[]> = () => { const rehypeRewriteTypelinks: RehypePlugin<[]> = () => (root: Html.Root): Html.Root => {
return (root: Html.Root): Html.Root => { visit(
visit( root as Unist.Parent,
root as Unist.Parent, "text",
"text", (node: Html.Text, index: number, parent: Html.Parent) => {
(node: Html.Text, index: number, parent: Html.Parent) => { let changed = false;
let changed = false;
node.value = node.value.replace( node.value = node.value.replace(
/TYPE99(\w+.)99TYPE/g, /TYPE99(\w+.)99TYPE/g,
(_full: string, match: string) => { (_full: string, match: string) => {
changed = true; changed = true;
const linkObject = getQMLTypeLinkObject(match); const linkObject = getQMLTypeLinkObject(match);
const link = getQMLTypeLink(linkObject); const link = getQMLTypeLink(currentVersion, linkObject);
const icon = const icon =
linkObject.mtype && linkObject.mtype !== "func" linkObject.mtype && linkObject.mtype !== "func"
? getIconForLink(linkObject.mtype, false) ? getIconForLink(linkObject.mtype, false)
: null; : null;
const hasParens = const hasParens =
linkObject.mtype === "func" || linkObject.mtype === "func" ||
linkObject.mtype === "signal"; linkObject.mtype === "signal";
const hasDot = linkObject.name && linkObject.mname; const hasDot = linkObject.name && linkObject.mname;
return `<a class="type${linkObject.mtype}-link typedata-link" href=${link}>${icon ?? ""}${linkObject.name ?? ""}${hasDot ? "." : ""}${linkObject.mname ?? ""}${hasParens ? "()" : ""}</a>`; return `<a class="type${linkObject.mtype}-link typedata-link" href=${link}>${icon ?? ""}${linkObject.name ?? ""}${hasDot ? "." : ""}${linkObject.mname ?? ""}${hasParens ? "()" : ""}</a>`;
}
);
if (changed) {
const fragment = fromHtml(node.value, {
fragment: true,
});
parent.children.splice(index, 1, ...fragment.children);
return SKIP;
} }
);
return CONTINUE; if (changed) {
const fragment = fromHtml(node.value, {
fragment: true,
});
parent.children.splice(index, 1, ...fragment.children);
return SKIP;
} }
);
return root; return CONTINUE;
}; }
);
return root;
};
const rehypeRewriteVersionedDoclinks: RehypePlugin<[]> = () => (root: Html.Root): Html.Root => {
visit(
root as Unist.Parent,
"element",
({ tagName, properties }: Html.Element) => {
if (tagName !== "a") return CONTINUE;
if (!(properties.href as string ?? "").startsWith("@docs")) return CONTINUE;
properties.href = `/docs/${currentVersion}/${(properties.href as string).slice(6)}`;
return CONTINUE;
}
);
return root;
}; };
const shikiRewriteTypelinks: ShikiTransformer = { const shikiRewriteTypelinks: ShikiTransformer = {
@ -123,7 +136,7 @@ const shikiRewriteTypelinks: ShikiTransformer = {
if (hasTypelinks) { if (hasTypelinks) {
code.replace(regExp, (_full: string, match: string) => { code.replace(regExp, (_full: string, match: string) => {
const linkObject = getQMLTypeLinkObject(match); const linkObject = getQMLTypeLinkObject(match);
const link = getQMLTypeLink(linkObject); const link = getQMLTypeLink(currentVersion, linkObject);
return `<a href=${link}>${linkObject.name ?? ""}</a>`; return `<a href=${link}>${linkObject.name ?? ""}</a>`;
}); });
} }
@ -166,6 +179,7 @@ export const markdownConfig: AstroMarkdownOptions = {
{ idPropertyName: "id" }, { idPropertyName: "id" },
], ],
rehypeRewriteTypelinks, rehypeRewriteTypelinks,
rehypeRewriteVersionedDoclinks,
], ],
}; };
@ -173,16 +187,18 @@ let globalMarkdownProcessor: Promise<MarkdownProcessor>;
async function getMarkdownProcessor(): Promise<MarkdownProcessor> { async function getMarkdownProcessor(): Promise<MarkdownProcessor> {
if (!globalMarkdownProcessor) { if (!globalMarkdownProcessor) {
globalMarkdownProcessor = globalMarkdownProcessor = createMarkdownProcessor(markdownConfig);
createMarkdownProcessor(markdownConfig);
} }
return globalMarkdownProcessor; return globalMarkdownProcessor;
} }
export async function processMarkdown( export async function processMarkdown(
markdown: string version: string,
markdown: string,
): Promise<string> { ): Promise<string> {
return (await (await getMarkdownProcessor()).render(markdown)) currentVersion = version;
.code; const r = (await (await getMarkdownProcessor()).render(markdown)).code;
currentVersion = "NOVERSION";
return r;
} }

View file

@ -8,12 +8,5 @@ const guide = defineCollection({
index: z.number(), index: z.number(),
}), }),
}); });
const version = defineCollection({
loader: glob({ pattern: "**/*", base: "src/docs" }),
schema: z.object({
title: z.string(),
index: z.number(),
}),
});
export const collections = { guide, version }; export const collections = { guide };

View file

@ -4,7 +4,7 @@ index: 2
--- ---
This page will walk you through the process of creating a simple bar/panel, and This page will walk you through the process of creating a simple bar/panel, and
introduce you to all the basic concepts involved. You can use the introduce you to all the basic concepts involved. You can use the
[QML Language Reference](/docs/guide/qml-language) to learn about the syntax [QML Language Reference](@docs/guide/qml-language) to learn about the syntax
of the QML language. of the QML language.
> [!NOTE] > [!NOTE]
@ -65,7 +65,7 @@ The above example creates a bar/panel on your currently focused monitor with
a centered piece of [text](https://doc.qt.io/qt-6/qml-qtquick-text.html). a centered piece of [text](https://doc.qt.io/qt-6/qml-qtquick-text.html).
It will also reserve space for itself on your monitor. It will also reserve space for itself on your monitor.
More information about available properties is available in the [type reference](/docs/types/Quickshell/PanelWindow). More information about available properties is available in the [type reference](@docs/types/Quickshell/PanelWindow).
## Running a process ## Running a process
@ -75,11 +75,11 @@ To start with, let's make a clock. To get the time, we'll use the `date` command
> [!note/Note] > [!note/Note]
> Quickshell can do more than just run processes. Read until the end for more information. > Quickshell can do more than just run processes. Read until the end for more information.
We can use a [Process](/docs/types/quickshell.io/process) object to run commands We can use a [Process](@docs/types/quickshell.io/process) object to run commands
and a @@Quickshell.Io.StdioCollector to read their output. and a @@Quickshell.Io.StdioCollector to read their output.
We'll listen to the @@Quickshell.Io.StdioCollector.streamFinished(s) signal with We'll listen to the @@Quickshell.Io.StdioCollector.streamFinished(s) signal with
a [signal handler](/docs/configuration/qml-overview/#signal-handlers) a [signal handler](@docs/guide/qml-language/#signal-handlers)
to update the text on the clock. to update the text on the clock.
> [!note/Note] > [!note/Note]
@ -252,14 +252,12 @@ import QtQuick
} }
``` ```
<span class="small"> See also: [Property Bindings](@docs/guide/qml-language#property-bindings),
See also: [Property Bindings](/docs/configuration/qml-overview/#property-bindings), [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
[Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)
</span>
With this example, bars will be created and destroyed as you plug and unplug them, With this example, bars will be created and destroyed as you plug and unplug them,
due to the reactive nature of the @@Quickshell.Quickshell.screens property. due to the reactive nature of the @@Quickshell.Quickshell.screens property.
(See: [Reactive Bindings](/docs/configuration/qml-overview/#reactive-bindings).) (See: [Reactive Bindings](@docs/guide/qml-language#reactive-bindings).)
Now there's an important problem you might have noticed: when the window Now there's an important problem you might have noticed: when the window
is created multiple times, we also make a new Process and Timer, which makes the is created multiple times, we also make a new Process and Timer, which makes the
@ -336,10 +334,10 @@ outside of its component. Remember, components can be created _any number of tim
including zero, so `clock` may not exist or there may be more than one, meaning including zero, so `clock` may not exist or there may be more than one, meaning
there isn't an object to refer to from here. there isn't an object to refer to from here.
We can fix it with a [Property Definition](/docs/configuration/qml-overview/#property-definitions). We can fix it with a [Property Definition](@docs/guide/qml-language#property-definitions).
We can define a property inside of the ShellRoot and reference it from the clock We can define a property inside of the ShellRoot and reference it from the clock
text instead. Due to QML's [Reactive Bindings](/docs/configuration/qml-overview/#reactive-bindings), text instead. Due to QML's [Reactive Bindings](@docs/guide/qml-language#reactive-bindings),
the clock text will be updated when we update the property for every clock that the clock text will be updated when we update the property for every clock that
currently exists. currently exists.
@ -409,7 +407,7 @@ above code; however, we can make it more concise:
component wrapping the window and place the window directly into the component wrapping the window and place the window directly into the
`delegate` property. `delegate` property.
2. The @@Quickshell.Variants.delegate property is a 2. The @@Quickshell.Variants.delegate property is a
[Default Property](/docs/configuration/qml-overview/#the-default-property), [Default Property](@docs/guide/qml-language#the-default-property),
which means we can skip the `delegate:` part of the assignment. which means we can skip the `delegate:` part of the assignment.
We're already using the default property of @@Quickshell.ShellRoot to store our We're already using the default property of @@Quickshell.ShellRoot to store our
Variants, Process, and Timer components among other things. Variants, Process, and Timer components among other things.
@ -533,11 +531,11 @@ import QtQuick
} }
``` ```
<span class="small">See also: [Scope](/docs/types/Quickshell/Scope/)</span> See also: @@Quickshell.Scope
Any qml file that starts with an uppercase letter can be referenced this way. Any qml file that starts with an uppercase letter can be referenced this way.
We can bring in other folders as well using We can bring in other folders as well using
[import statements](/docs/configuration/qml-overview/#explicit-imports). [import statements](@docs/guide/qml-language#explicit-imports).
Now what about breaking out the clock? This is a bit more complex because Now what about breaking out the clock? This is a bit more complex because
the clock component in the bar need to be dealt with, as well as the necessary the clock component in the bar need to be dealt with, as well as the necessary
@ -683,7 +681,7 @@ import Quickshell
Now you might be thinking, why do we need the `Time` type in Now you might be thinking, why do we need the `Time` type in
our bar file, and the answer is we don't. We can make `Time` our bar file, and the answer is we don't. We can make `Time`
a [Singleton](/docs/configuration/qml-overview/#singletons). a [Singleton](@docs/guide/qml-language#singletons).
A singleton object has only one instance, and is accessible from A singleton object has only one instance, and is accessible from
any scope. any scope.

View file

@ -7,10 +7,7 @@ import Collapsible from "@components/Collapsible.astro";
Quickshell is configured using the Qt Modeling Language, or QML. Quickshell is configured using the Qt Modeling Language, or QML.
This page explains what you need to know about QML to start using Quickshell. This page explains what you need to know about QML to start using Quickshell.
<span class="small"> See also: [Qt Documentation: QML Tutorial](https://doc.qt.io/qt-6/qml-tutorial.html)
See also: [Qt Documentation: QML
Tutorial](https://doc.qt.io/qt-6/qml-tutorial.html)
</span>
## Structure ## Structure
@ -143,10 +140,7 @@ import QtQuick.Layouts 6.0 as L
import "jsfile.js" as JsFile import "jsfile.js" as JsFile
``` ```
<span class="small"> See also: [Qt Documentation: Import syntax](https://doc.qt.io/qt-6/qtqml-syntax-imports.html)
See also: [Qt Documentation: Import
syntax](https://doc.qt.io/qt-6/qtqml-syntax-imports.html)
</span>
#### Implicit imports #### Implicit imports
@ -222,7 +216,7 @@ All property bindings are [_reactive_](#reactive-bindings). This means that when
property the expression depends on is updated, the expression is re-evaluated and the property property the expression depends on is updated, the expression is re-evaluated and the property
is updated accordingly. is updated accordingly.
<span class="small">See also [Reactive bindings](#reactive-bindings) for more information</span> See also [Reactive bindings](#reactive-bindings) for more information
##### Property definitions ##### Property definitions
@ -374,10 +368,7 @@ all other objects, you should refer to them by id when accessing properties.
} }
``` ```
<span class="small"> See also: [Qt Documentation: Scope and Naming Resolution](https://doc.qt.io/qt-6/qtqml-documents-scope.html)
See also: [Qt Documentation: Scope and Naming
Resolution](https://doc.qt.io/qt-6/qtqml-documents-scope.html)
</span>
#### Functions #### Functions
@ -486,10 +477,7 @@ A signal is basically an event emitter you can connect to and receive updates fr
They can be declared everywhere [properties](#properties) and [functions](#functions) They can be declared everywhere [properties](#properties) and [functions](#functions)
can, and follow the same [scoping rules](#property-access-scopes). can, and follow the same [scoping rules](#property-access-scopes).
<span class="small"> See also: [Qt Documentation: Signal and Handler Event System](https://doc.qt.io/qt-6/qtqml-syntax-signals.html)
See also: [Qt Documentation: Signal and Handler Event
System](https://doc.qt.io/qt-6/qtqml-syntax-signals.html)
</span>
##### Signal definitions ##### Signal definitions
@ -529,11 +517,8 @@ or signal when the signal is emitted.
} }
``` ```
<span class="small"> `Component.onCompleted` will be addressed later in [Attached Properties](#attached-properties),
`Component.onCompleted` will be addressed later in [Attached but for now, just know that it runs immediately once the object is fully initialized.
Properties](#attached-properties), but for now, just know that it runs
immediately once the object is fully initialized.
</span>
When the button is clicked, the button emits the @@QtQuick.Controls.Button.clicked(s) When the button is clicked, the button emits the @@QtQuick.Controls.Button.clicked(s)
signal, which we connected to `updateText`. The signal then invokes `updateText`, signal, which we connected to `updateText`. The signal then invokes `updateText`,
@ -788,11 +773,9 @@ files.
### Reactive bindings ### Reactive bindings
<span class="small"> This section assumes knowledge of: [Properties](#properties), [Signals](#signals),
This section assumes knowledge of: [Properties](#properties), and [Functions](#functions).
[Signals](#signals), and [Functions](#functions). See also the [Qt See also the [Qt documentation](https://doc.qt.io/qt-6/qtqml-syntax-propertybinding.html).
documentation](https://doc.qt.io/qt-6/qtqml-syntax-propertybinding.html).
</span>
Reactivity is when a property is updated based on updates to another property. Reactivity is when a property is updated based on updates to another property.
Every time one of the properties in a binding change, the binding is re-evaluated Every time one of the properties in a binding change, the binding is re-evaluated

View file

@ -19,38 +19,30 @@ interface Props {
} }
const { title, description, headings, type } = Astro.props; const { title, description, headings, type } = Astro.props;
const url = Astro.url.pathname.split("/"); let url = Astro.url.pathname.split("/").filter(s => s !== "");
const customBreadcrumbs = [ const breadcrumbs = [{
{ text: "custom",
index: 0, href: "/",
text: "custom", }];
href: "/",
},
{
text: url[1].slice(0, 1)[0].toUpperCase() + url[1].slice(1),
href: `/${url[1]}`,
},
];
if (url[2]) { let linkPath = "";
customBreadcrumbs.push({ if (url[0] === "docs") {
text: url[2].slice(0, 1)[0].toUpperCase() + url[2].slice(1), const { version } = Astro.params;
href: `/${url[1]}/${url[2]}`, linkPath = `/docs/${version}`;
breadcrumbs.push({
text: `Docs (${version})`,
href: linkPath,
});
url = url.slice(2);
}
for (const segment of url) {
linkPath += `/${segment}`;
breadcrumbs.push({
text: segment[0].toUpperCase() + segment.slice(1),
href: linkPath,
}); });
if (url[3]) {
customBreadcrumbs.push({
text: url[3].slice(0, 1)[0].toUpperCase() + url[3].slice(1),
href: `/${url[1]}/${url[2]}/${url[3]}`,
});
if (url[4]) {
customBreadcrumbs.filter((_, index) => index !== 4);
customBreadcrumbs.push({
text: url[4],
href: `/${url[1]}/${url[2]}/${url[3]}/${url[4]}`,
});
}
}
} }
--- ---
@ -65,7 +57,7 @@ if (url[2]) {
<div class="docslayout-root"> <div class="docslayout-root">
<Nav mobile={false}/> <Nav mobile={false}/>
<div class="docslayout-inner" data-pagefind-body> <div class="docslayout-inner" data-pagefind-body>
<Breadcrumbs crumbs={customBreadcrumbs} linkTextFormat="sentence" truncated={true} data-pagefind-ignore> <Breadcrumbs crumbs={breadcrumbs} linkTextFormat="sentence" truncated={true} data-pagefind-ignore>
<svg <svg
slot="index" slot="index"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -0,0 +1,27 @@
---
import GuideLayout from "@layouts/GuideLayout.astro";
import { getVersionsData } from "@config/io/generateTypeData";
import { processMarkdown } from "@config/io/markdown";
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const { versions } = await getVersionsData();
const guidePages = await getCollection("guide");
// versioned guides unhandled for now
return versions.flatMap(version => guidePages.map(page => ({
params: { version: version.name, id: page.id == "index" ? "/" : page.id },
props: { version, page },
})));
}
const { version, page } = Astro.props;
const { headings } = await render(page);
// we can't use 'Content' because there isn't a way to pass in a version
const html = await processMarkdown(version.name, page.body!);
---
<GuideLayout title={page.data.title} description="" headings={headings}>
<Fragment set:html={html}/>
</GuideLayout>

View file

@ -0,0 +1,20 @@
---
import DocsLayout from "@layouts/DocsLayout.astro";
import { getVersionsData } from "@config/io/generateTypeData";
export async function getStaticPaths() {
return (await getVersionsData()).versions.map(version => ({
params: { version: version.name },
props: { version },
}));
}
const { version } = Astro.props;
---
<DocsLayout title="Quickshell Docs" description="Quickshell Documentation">
<h2>Docs</h2>
<div class="root-nav">
<h3><a href={`/docs/${version.name}/guide`}>Usage Guide</a></h3>
<h3><a href={`/docs/${version.name}/types`}>Type Definitions</a></h3>
</div>
</DocsLayout>

View file

@ -1,7 +1,7 @@
--- ---
import { getQMLTypeLink } from "@config/io/helpers"; import { getQMLTypeLink } from "@config/io/helpers";
import { processMarkdown } from "@config/io/markdown"; import { processMarkdown } from "@config/io/markdown";
import { getModulesData } from "@config/io/generateTypeData"; import { getVersionsData } from "@config/io/generateTypeData";
import DocsLayout from "@layouts/DocsLayout.astro"; import DocsLayout from "@layouts/DocsLayout.astro";
import TOC from "@components/navigation/sidebars/TOC.astro"; import TOC from "@components/navigation/sidebars/TOC.astro";
import Properties from "@components/type/Properties.astro"; import Properties from "@components/type/Properties.astro";
@ -11,19 +11,22 @@ import Variants from "@components/type/Variants.astro";
import Badge from "@components/Badge.astro"; import Badge from "@components/Badge.astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const modules = await getModulesData(); return (await getVersionsData()).versions.flatMap(version => {
return modules.flatMap(module => module.types.map(type => ({ return version.modules.flatMap(module => {
params: { module: module.name, type: type.name }, return module.types.map(type => ({
props: { module, type } params: { version: version.name, module: module.name, type: type.name },
}))); props: { version, module, type },
}));
});
});
} }
const { module, type } = Astro.props; const { version, module, type } = Astro.props;
const superLink = type.super ? getQMLTypeLink(type.super) : null; const superLink = type.super ? getQMLTypeLink(version.name, type.super) : null;
const details = type.details const details = type.details
? await processMarkdown(type.details) ? await processMarkdown(version.name, type.details)
: null; : null;
--- ---
<DocsLayout title={`${module.name} - ${type.name}`} description={type.description ?? ""} type={type}> <DocsLayout title={`${module.name} - ${type.name}`} description={type.description ?? ""} type={type}>
@ -57,17 +60,17 @@ const details = type.details
{details ? <span class="parsedMD" set:html={details}/> : (<span class="toparse">{type.description}</span>)} {details ? <span class="parsedMD" set:html={details}/> : (<span class="toparse">{type.description}</span>)}
</subheading> </subheading>
{ Object.keys(type.properties ?? {}).length != 0 && ( { Object.keys(type.properties ?? {}).length != 0 && (
<h2>Properties <a href="/docs/guide/qml-language#properties">[?]</a></h2> <h2>Properties <a href={`/docs/${version.name}/guide/qml-language#properties`}>[?]</a></h2>
<Properties props={type.properties!}/> <Properties props={type.properties!}/>
)} )}
{ (type.functions?.length ?? 0) != 0 && ( { (type.functions?.length ?? 0) != 0 && (
<h2>Functions <a href="/docs/guide/qml-language#functions">[?]</a></h2> <h2>Functions <a href={`/docs/${version.name}guide/qml-language#functions`}>[?]</a></h2>
<Functions <Functions
funcData={type.functions!} funcData={type.functions!}
/> />
)} )}
{ Object.keys(type.signals ?? {}).length != 0 && ( { Object.keys(type.signals ?? {}).length != 0 && (
<h2>Signals <a href="/docs/guide/qml-language#signals">[?]</a></h2> <h2>Signals <a href={`/docs/${version.name}guide/qml-language#signals`}>[?]</a></h2>
<Signals <Signals
signals={type.signals!} signals={type.signals!}
/> />

View file

@ -1,19 +1,20 @@
--- ---
import DocsLayout from "@layouts/DocsLayout.astro"; import DocsLayout from "@layouts/DocsLayout.astro";
import { getModulesData } from "@config/io/generateTypeData"; import { getVersionsData } from "@config/io/generateTypeData";
import { processMarkdown } from "@src/config/io/markdown"; import { processMarkdown } from "@src/config/io/markdown";
export async function getStaticPaths() { export async function getStaticPaths() {
const modules = await getModulesData(); return (await getVersionsData()).versions.flatMap(version => {
return modules.map(module => ({ return version.modules.map(module => ({
params: { module: module.name }, params: { version: version.name, module: module.name },
props: { module }, props: { version, module },
})); }));
});
} }
const { module } = Astro.props; const { version, module } = Astro.props;
const details = module.details const details = module.details
? await processMarkdown(module.details) ? await processMarkdown(version.name, module.details)
: null; : null;
--- ---
@ -30,7 +31,7 @@ const details = module.details
{module.types.map(type => {module.types.map(type =>
( (
<div class="root-nav-entry"> <div class="root-nav-entry">
<a class="root-nav-link" href={`/docs/types/${module.name}/${type.name}`}> <a class="root-nav-link" href={`/docs/${version.name}/types/${module.name}/${type.name}`}>
{type.name} {type.name}
</a> </a>
<span class="root-nav-desc">{type.description}</span> <span class="root-nav-desc">{type.description}</span>

View file

@ -1,8 +1,15 @@
--- ---
import DocsLayout from "@layouts/DocsLayout.astro"; import DocsLayout from "@layouts/DocsLayout.astro";
import { getModulesData } from "@config/io/generateTypeData"; import { getVersionsData } from "@config/io/generateTypeData";
const modules = await getModulesData(); export async function getStaticPaths() {
return (await getVersionsData()).versions.map(version => ({
params: { version: version.name },
props: { version },
}));
}
const { version } = Astro.props;
--- ---
<DocsLayout title="Quickshell Module Listing" description="Quickshell Type Documentation"> <DocsLayout title="Quickshell Module Listing" description="Quickshell Type Documentation">
<div class="docs-content"> <div class="docs-content">
@ -11,9 +18,9 @@ const modules = await getModulesData();
<section> <section>
<span>All modules included with Quickshell</span> <span>All modules included with Quickshell</span>
<div class="root-nav" data-pagefind-ignore> <div class="root-nav" data-pagefind-ignore>
{modules.map(module => ( {version.modules.map(module => (
<div class="root-nav-entry"> <div class="root-nav-entry">
<a class="root-nav-link" href={`/docs/types/${module.name}`}> <a class="root-nav-link" href={`/docs/${version.name}/types/${module.name}`}>
{module.name} {module.name}
</a> </a>
<span class="root-nav-desc">{module.description}</span> <span class="root-nav-desc">{module.description}</span>

View file

@ -1,20 +0,0 @@
---
import GuideLayout from "@layouts/GuideLayout.astro";
import { getCollection, render } from "astro:content";
export async function getStaticPaths() {
const guidePages = await getCollection("guide");
return guidePages.map(page => ({
params: { id: page.id == "index" ? "/" : page.id },
props: { page },
}));
}
const { page } = Astro.props;
const { Content, headings } = await render(page);
---
<GuideLayout title={page.data.title} description="" headings={headings}>
<Content/>
</GuideLayout>

View file

@ -1,10 +0,0 @@
---
import DocsLayout from "@layouts/DocsLayout.astro";
---
<DocsLayout title="Quickshell Docs" description="Quickshell Documentation">
<h2>Docs</h2>
<div class="root-nav">
<h3><a href="/docs/guide">Usage Guide</a></h3>
<h3><a href="/docs/types">Type Definitions</a></h3>
</div>
</DocsLayout>

View file

@ -3,6 +3,8 @@ import Footer from "@src/components/Footer.astro";
import BaseLayout from "@layouts/BaseLayout.astro"; import BaseLayout from "@layouts/BaseLayout.astro";
import Marquee from "@components/marquee/Marquee.astro"; import Marquee from "@components/marquee/Marquee.astro";
import FeatureList from "@src/components/featurelist/FeatureList.astro"; import FeatureList from "@src/components/featurelist/FeatureList.astro";
import { getVersionsData } from "@config/io/generateTypeData";
const defaultVersion = (await getVersionsData()).default;
const title = "Quickshell"; const title = "Quickshell";
--- ---
@ -28,10 +30,10 @@ const title = "Quickshell";
</p> </p>
</div> </div>
<div class="about-buttons"> <div class="about-buttons">
<a href="/docs/guide/install-setup" class="main-page_link-card"> <a href={`/docs/${defaultVersion}/guide/install-setup`} class="main-page_link-card">
<h3>Install</h3> <h3>Install</h3>
</a> </a>
<a href="/docs/types" class="main-page_link-card main-page_bluecard"> <a href={`/docs/${defaultVersion}/types`} class="main-page_link-card main-page_bluecard">
<h3>Documentation</h3> <h3>Documentation</h3>
</a> </a>
</div> </div>

View file

@ -1,16 +1,16 @@
{ {
"default": "master", "default": "v0.2.0",
"versions": [ "versions": [
{ {
"name": "master", "name": "master",
"types": "./modules" "types": "./modules"
}, },
{ {
"name": "0.2.0", "name": "v0.2.0",
"types": "./modules" "types": "./modules"
}, },
{ {
"name": "0.1.0", "name": "v0.1.0",
"types": "./modules" "types": "./modules"
} }
] ]