Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🗂 Add site option to include folder structure in url paths #1601

Merged
merged 10 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shiny-dragons-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-cli': patch
'mystmd': patch
---

Add site option to include folder structure in url paths
1 change: 1 addition & 0 deletions packages/myst-cli/src/process/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ export async function transformMdast(
url = `/${useSlug ? pageSlug : ''}`;
dataUrl = `/${pageSlug}.json`;
}
url = url?.replace('.', '/');
rowanc1 marked this conversation as resolved.
Show resolved Hide resolved
updateFileInfoFromFrontmatter(session, file, frontmatter, url, dataUrl);
const data: RendererData = {
kind: isJupytext ? SourceFileKind.Notebook : kind,
Expand Down
4 changes: 2 additions & 2 deletions packages/myst-cli/src/process/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,14 @@ export async function writeMystSearchJson(session: ISession, pages: LocalProject
.map((page) => selectFile(session, page.file))
.map((file) => {
const { mdast, slug, frontmatter } = file ?? {};
if (!mdast || !frontmatter) {
if (!mdast || !frontmatter || !slug) {
return [];
}
const title = frontmatter.title ?? '';

// Group by section (simple running accumulator)
const sections = toSectionedParts(mdast);
const pageURL = slug && DEFAULT_INDEX_FILENAMES.includes(slug) ? '/' : `/${slug}`;
const pageURL = DEFAULT_INDEX_FILENAMES.includes(slug) ? '/' : `/${slug.replace('.', '/')}`;
rowanc1 marked this conversation as resolved.
Show resolved Hide resolved
// Build sections into search records
return sections
.map((section, index) => {
Expand Down
21 changes: 14 additions & 7 deletions packages/myst-cli/src/project/fromPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isDirectory } from 'myst-cli-utils';
import { RuleId } from 'myst-common';
import type { ISession } from '../session/types.js';
import { addWarningForFile } from '../utils/addWarningForFile.js';
import { fileInfo } from '../utils/fileInfo.js';
import { fileInfo, fileTitle } from '../utils/fileInfo.js';
import { nextLevel } from '../utils/nextLevel.js';
import { VALID_FILE_EXTENSIONS, isValidFile } from '../utils/resolveExtension.js';
import { shouldIgnoreFile } from '../utils/shouldIgnoreFile.js';
Expand All @@ -20,9 +20,10 @@ import type {
LocalProjectPage,
LocalProject,
PageSlugs,
SlugOptions,
} from './types.js';

type Options = {
type Options = SlugOptions & {
ignore?: string[];
suppressWarnings?: boolean;
};
Expand Down Expand Up @@ -54,7 +55,7 @@ function projectPagesFromPath(
try {
// TODO: We don't yet have a way to do nested tocs with new-style toc
session.log.debug(`Respecting legacy TOC in subdirectory: ${join(path, '_toc.yml')}`);
return pagesFromSphinxTOC(session, path, prevLevel);
return pagesFromSphinxTOC(session, path, prevLevel, opts);
} catch {
if (!suppressWarnings) {
addWarningForFile(
Expand All @@ -73,15 +74,18 @@ function projectPagesFromPath(
return {
file,
level,
slug: fileInfo(file, pageSlugs).slug,
slug: fileInfo(file, pageSlugs, opts).slug,
implicit: true,
} as LocalProjectPage;
});
const folders = contents
.filter((file) => isDirectory(file))
.sort(comparePaths)
.map((dir) => {
const projectFolder: LocalProjectFolder = { title: fileInfo(dir, pageSlugs).title, level };
const projectFolder: LocalProjectFolder = {
title: fileTitle(dir),
level,
};
const pages = projectPagesFromPath(session, dir, nextLevel(level), pageSlugs, opts);
if (!pages.length) {
return [];
Expand Down Expand Up @@ -145,13 +149,15 @@ export function projectFromPath(
session: ISession,
path: string,
indexFile?: string,
opts?: SlugOptions,
): Omit<LocalProject, 'bibliography'> {
const ext_string = VALID_FILE_EXTENSIONS.join(' or ');
if (indexFile) {
if (!isValidFile(indexFile))
throw Error(`Index file ${indexFile} has invalid extension; must be ${ext_string}}`);
if (!fs.existsSync(indexFile)) throw Error(`Index file ${indexFile} not found`);
}
if (opts?.urlFolders && !opts.projectPath) opts.projectPath = path;
const ignoreFiles = getIgnoreFiles(session, path);
let implicitIndex = false;
if (!indexFile) {
Expand All @@ -160,7 +166,7 @@ export function projectFromPath(
path,
1,
{},
{ ignore: ignoreFiles, suppressWarnings: true },
{ ...opts, ignore: ignoreFiles, suppressWarnings: true },
);
if (!searchPages.length) {
throw Error(`No valid files with extensions ${ext_string} found in path "${path}"`);
Expand All @@ -170,8 +176,9 @@ export function projectFromPath(
implicitIndex = true;
}
const pageSlugs: PageSlugs = {};
const { slug } = fileInfo(indexFile, pageSlugs);
const { slug } = fileInfo(indexFile, pageSlugs, { ...opts, session });
const pages = projectPagesFromPath(session, path, 1, pageSlugs, {
...opts,
ignore: [indexFile, ...ignoreFiles],
});
return { file: indexFile, index: slug, path, pages, implicitIndex };
Expand Down
55 changes: 44 additions & 11 deletions packages/myst-cli/src/project/fromTOC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
LocalProjectPage,
LocalProject,
PageSlugs,
SlugOptions,
} from './types.js';
import type {
TOC,
Expand Down Expand Up @@ -160,6 +161,7 @@ function pagesFromEntries(
pages: (LocalProjectFolder | LocalProjectPage)[] = [],
level: PageLevels = 1,
pageSlugs: PageSlugs,
opts?: SlugOptions,
): (LocalProjectFolder | LocalProjectPage)[] {
const configFile = selectors.selectLocalConfigFile(session.store.getState(), path);
for (const entry of entries) {
Expand All @@ -174,7 +176,7 @@ function pagesFromEntries(
});
});
if (file && fs.existsSync(file) && !isDirectory(file)) {
const { slug } = fileInfo(file, pageSlugs);
const { slug } = fileInfo(file, pageSlugs, { ...opts, session });
pages.push({ file, level: entryLevel, slug, implicit: entry.implicit });
}
} else if (isURL(entry)) {
Expand Down Expand Up @@ -203,6 +205,7 @@ function pagesFromEntries(
pages,
nextLevel(entryLevel),
pageSlugs,
opts,
);
}
}
Expand Down Expand Up @@ -240,6 +243,7 @@ export function projectFromTOC(
toc: TOC,
level: PageLevels = 1,
file?: string,
opts?: SlugOptions,
): Omit<LocalProject, 'bibliography'> {
const pageSlugs: PageSlugs = {};
const ignoreFiles = [...getIgnoreFiles(session, path), ...listExplicitFiles(toc, path)];
Expand Down Expand Up @@ -268,9 +272,10 @@ export function projectFromTOC(
if (!indexFile) {
throw Error(`Could not resolve project index file: ${root.file}`);
}
const { slug } = fileInfo(indexFile, pageSlugs);
if (opts?.urlFolders && !opts.projectPath) opts.projectPath = path;
const { slug } = fileInfo(indexFile, pageSlugs, { ...opts, session });
const pages: (LocalProjectFolder | LocalProjectPage)[] = [];
pagesFromEntries(session, path, entries, pages, level, pageSlugs);
pagesFromEntries(session, path, entries, pages, level, pageSlugs, opts);
return { path: path || '.', file: indexFile, index: slug, pages };
}

Expand All @@ -281,14 +286,15 @@ function pagesFromSphinxChapters(
pages: (LocalProjectFolder | LocalProjectPage)[] = [],
level: PageLevels = 1,
pageSlugs: PageSlugs,
opts?: SlugOptions,
): (LocalProjectFolder | LocalProjectPage)[] {
const filename = tocFile(path);
const { dir } = parse(filename);
chapters.forEach((chapter) => {
// TODO: support globs and urls
const file = chapter.file ? resolveExtension(join(dir, chapter.file)) : undefined;
if (file) {
const { slug } = fileInfo(file, pageSlugs);
const { slug } = fileInfo(file, pageSlugs, { ...opts, session });
pages.push({ file, level, slug });
}
if (!file && chapter.file) {
Expand All @@ -304,7 +310,15 @@ function pagesFromSphinxChapters(
pages.push({ level, title: chapter.title });
}
if (chapter.sections) {
pagesFromSphinxChapters(session, path, chapter.sections, pages, nextLevel(level), pageSlugs);
pagesFromSphinxChapters(
session,
path,
chapter.sections,
pages,
nextLevel(level),
pageSlugs,
opts,
);
}
});
return pages;
Expand All @@ -323,6 +337,7 @@ export function projectFromSphinxTOC(
session: ISession,
path: string,
level: PageLevels = 1,
opts?: SlugOptions,
): Omit<LocalProject, 'bibliography'> {
const filename = tocFile(path);
if (!fs.existsSync(filename)) {
Expand All @@ -342,16 +357,17 @@ export function projectFromSphinxTOC(
).join('\n- ')}\n`,
);
}
const { slug } = fileInfo(indexFile, pageSlugs);
if (opts?.urlFolders && !opts.projectPath) opts.projectPath = path;
const { slug } = fileInfo(indexFile, pageSlugs, { ...opts, session });
const pages: (LocalProjectFolder | LocalProjectPage)[] = [];
if (toc.sections) {
// Do not allow sections to have level < 1
if (level < 1) level = 1;
pagesFromSphinxChapters(session, path, toc.sections, pages, level, pageSlugs);
pagesFromSphinxChapters(session, path, toc.sections, pages, level, pageSlugs, opts);
} else if (toc.chapters) {
// Do not allow chapters to have level < 0
if (level < 0) level = 0;
pagesFromSphinxChapters(session, path, toc.chapters, pages, level, pageSlugs);
pagesFromSphinxChapters(session, path, toc.chapters, pages, level, pageSlugs, opts);
} else if (toc.parts) {
// Do not allow parts to have level < -1
if (level < -1) level = -1;
Expand All @@ -360,7 +376,15 @@ export function projectFromSphinxTOC(
pages.push({ title: part.caption || `Part ${index + 1}`, level });
}
if (part.chapters) {
pagesFromSphinxChapters(session, path, part.chapters, pages, nextLevel(level), pageSlugs);
pagesFromSphinxChapters(
session,
path,
part.chapters,
pages,
nextLevel(level),
pageSlugs,
opts,
);
}
});
}
Expand All @@ -377,8 +401,16 @@ export function pagesFromTOC(
path: string,
toc: TOC,
level: PageLevels,
opts?: SlugOptions,
): (LocalProjectFolder | LocalProjectPage)[] {
const { file, index, pages } = projectFromTOC(session, path, toc, nextLevel(level));
const { file, index, pages } = projectFromTOC(
session,
path,
toc,
nextLevel(level),
undefined,
opts,
);
pages.unshift({ file, slug: index, level });
return pages;
}
Expand All @@ -392,8 +424,9 @@ export function pagesFromSphinxTOC(
session: ISession,
path: string,
level: PageLevels,
opts?: SlugOptions,
): (LocalProjectFolder | LocalProjectPage)[] {
const { file, index, pages } = projectFromSphinxTOC(session, path, nextLevel(level));
const { file, index, pages } = projectFromSphinxTOC(session, path, nextLevel(level), opts);
pages.unshift({ file, slug: index, level });
return pages;
}
10 changes: 7 additions & 3 deletions packages/myst-cli/src/project/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ export async function loadProjectFromDisk(
let newProject: Omit<LocalProject, 'bibliography'> | undefined;
let { index, writeTOC } = opts || {};
let legacyToc = false;
const siteConfig = selectors.selectLocalSiteConfig(state, path);
const urlFolders = !!siteConfig?.options?.url_folders;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to take suggestions about a better name for this option...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fwkoch do we have a policy about snake_case vs pascalCase for frontmatter/config item names?

Copy link
Contributor

@stevejpurves stevejpurves Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snake case for items in config data files (.yml), and so they also leak into typescript in places. Everything else is PascalCase (classes) / camelCase (fns, vars).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets do site.folders and get around it!

const sphinxTOCFile = validateSphinxTOC(session, path) ? tocFile(path) : undefined;
if (projectConfig?.toc !== undefined) {
newProject = projectFromTOC(session, path, projectConfig.toc, 1, projectConfigFile);
newProject = projectFromTOC(session, path, projectConfig.toc, 1, projectConfigFile, {
urlFolders,
});
if (sphinxTOCFile) {
addWarningForFile(
session,
Expand Down Expand Up @@ -94,14 +98,14 @@ export async function loadProjectFromDisk(
// },
// );
}
newProject = projectFromSphinxTOC(session, path);
newProject = projectFromSphinxTOC(session, path, undefined, { urlFolders });
} else {
const project = selectors.selectLocalProject(state, path);
if (!index && !project?.implicitIndex && project?.file) {
// If there is no new index, keep the original unless it was implicit previously
index = project.file;
}
newProject = projectFromPath(session, path, index);
newProject = projectFromPath(session, path, index, { urlFolders });
}
if (!newProject) {
throw new Error(`Could not load project from ${path}`);
Expand Down
Loading
Loading