Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
3y3 committed Dec 10, 2024
1 parent 1edad0f commit f65e1b1
Show file tree
Hide file tree
Showing 39 changed files with 1,525 additions and 1,157 deletions.
199 changes: 199 additions & 0 deletions src/commands/build/core/toc/TocService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import type { BuildConfig, Run } from '~/commands/build';

Check failure on line 1 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Replace `·BuildConfig,·Run·` with `BuildConfig,·Run`
import type { IncluderOptions, RawToc, RawTocItem, WithItems } from './types';

Check failure on line 2 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Replace `·IncluderOptions,·RawToc,·RawTocItem,·WithItems·` with `IncluderOptions,·RawToc,·RawTocItem,·WithItems`

import { ok } from 'node:assert';

Check failure on line 4 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Replace `·ok·` with `ok`
import { basename, dirname, join } from 'node:path';

Check failure on line 5 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Replace `·basename,·dirname,·join·` with `basename,·dirname,·join`
import { dump, load } from 'js-yaml';

Check failure on line 6 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Replace `·dump,·load·` with `dump,·load`
import { AsyncParallelHook, AsyncSeriesWaterfallHook, HookMap } from 'tapable';
import { freeze, intercept, isExternalHref, own } from '~/utils';

import loader, {IncludeMode, LoaderContext} from './loader';

export type TocServiceConfig = {
ignoreStage: BuildConfig['ignoreStage'];
template: BuildConfig['template'];
removeHiddenTocItems: BuildConfig['removeHiddenTocItems'];
};

type WalkStepResult = RawTocItem | RawTocItem[] | void;

type TocServiceHooks = {
/**
* Called before item data processing (but after data interpolation)
*/
Item: AsyncSeriesWaterfallHook<[RawTocItem, RelativePath]>;
Includer: HookMap<AsyncSeriesWaterfallHook<[RawToc, IncluderOptions, RelativePath]>>;
Resolved: AsyncParallelHook<[Toc, RelativePath]>;
Included: AsyncParallelHook<[Toc, LoadFrom]>;
};

type IncludeInfo = {
from: RelativePath;
mode: IncludeMode;
mergeBase?: RelativePath;
};

// TODO: addSourcePath(fileContent, sourcePath);
export class TocService {
hooks: TocServiceHooks;

private run: Run;

private logger: Run['logger'];

private vars: Run['vars'];

private config: TocServiceConfig;

private tocs: Map<RelativePath, Toc> = new Map();

private entries: Set<RelativePath> = new Set();

constructor(run: Run) {
this.run = run;
this.logger = run.logger;
this.vars = run.vars;
this.config = run.config;
this.hooks = intercept('TocService', {
Item: new AsyncSeriesWaterfallHook(['item', 'path']),
Includer: new HookMap(() => new AsyncSeriesWaterfallHook(['toc', 'options', 'path'])),
Resolved: new AsyncParallelHook(['toc', 'path']),
Included: new AsyncParallelHook(['toc', 'from']),
});
}

// TODO: remove after metadate refactoring
async realpath(path: RelativePath) {
return this.run.realpath(join(this.run.input, path));
}

async load(path: RelativePath, include?: IncludeInfo) {
this.logger.proc(path);

const file = join(this.run.input, path);

ok(file.startsWith(this.run.input), `Requested toc '${file}' is out of project scope.`);

const context: LoaderContext = {
root: this.run.input,
mode: include?.mode || IncludeMode.RootMerge,
base: include?.from,
mergeBase: include?.mergeBase,
path,
vars: await this.vars.load(path),
toc: this,
options: {
ignoreStage: this.config.ignoreStage,
resolveConditions: this.config.template.features.conditions,
resolveSubstitutions: this.config.template.features.substitutions,
removeHiddenItems: this.config.removeHiddenTocItems,
},
};

const content = load(await this.run.read(file)) as RawToc;

if (include && [IncludeMode.RootMerge, IncludeMode.Merge].includes(include.mode)) {
const from = dirname(file);
const to = join(this.run.input, include.mergeBase || dirname(include.from));
await this.run.copy(from, to, [basename(file)]);
}

const toc = await loader.call(context, content);

// If this is not a part of other toc.yaml
if (!include) {

Check warning on line 104 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected negated condition
// TODO: we don't need to store tocs in future
// All processing should subscribe on toc.hooks.Resolved
this.tocs.set(path, toc);
await this.walkItems([toc], (item) => {
if (own(item, 'href') && !isExternalHref(item.href)) {
this.entries.add(join(dirname(path), item.href));
}

return item;
});

freeze(toc);

await this.hooks.Resolved.promise(toc, path);
} else {
await this.hooks.Included.promise(toc, include);
}

return toc;
}

dump(toc: Toc) {
return dump(toc);
}

async walkItems<T extends RawTocItem[] | undefined>(
items: T,
actor: (item: RawTocItem) => Promise<WalkStepResult> | WalkStepResult,
): Promise<T> {
if (!items || !items.length) {
return items;
}

const results: RawTocItem[] = [];
const queue = [...items];
while (queue.length) {
const item = queue.shift() as RawTocItem;

const result = await actor(item);
if (result !== undefined) {
if (Array.isArray(result)) {
results.push(...result);
} else {
results.push(result);
}
}

if (hasItems(result)) {
result.items = await this.walkItems(result.items, actor);
}
}

return results as T;
}

/**
* Resolves toc path and data for any page path
*
* @param {RelativePath} path - any page path
*
* @returns [RelativePath, Toc]
*/
for(path: RelativePath): [RelativePath, Toc] {
// TODO: assert relative

if (!path) {
throw new Error('Error while finding toc dir.');
}

const tocPath = join(dirname(path), 'toc.yaml');

if (this.tocs.has(tocPath)) {
return [tocPath, this.tocs.get(tocPath)];
}

return this.for(dirname(path));
}

async applyIncluder(path: RelativePath, name: string, options: IncluderOptions) {
const hook = this.hooks.Includer.get(name);

ok(hook, `Includer with name '${name}' is not registered.`);

const toc = await hook.promise({}, options, path);

await this.run.write(
join(this.run.input, options.path),
this.run.toc.dump(toc)
);
}
}

function hasItems(item: any): item is WithItems {

Check warning on line 197 in src/commands/build/core/toc/TocService.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type
return item && typeof item === 'object' && item.items && item.items.length;
}
147 changes: 147 additions & 0 deletions src/commands/build/core/toc/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`toc-loader > includes > should merge deep includes in link mode 1`] = `
"items:
- name: Outer Item
items:
- name: Inner Item 1
href: _includes/core/item-1.md
- name: Inner Item 2
items:
- name: Inner Lib Item 1
href: _includes/lib/item-1.md
"
`;

exports[`toc-loader > includes > should merge deep includes in merge mode 1`] = `
"items:
- name: Outer Item
items:
- name: Inner Item 1
href: _includes/core/item-1.md
- name: Inner Item 2
items:
- name: Inner Merge Item 1
href: _includes/core/merge-item-1.md
- name: Inner Deep Merge Item 1
href: _includes/core/deep-merge-item-1.md
- name: Inner Sub Item 1
href: _includes/core/sub-item-1.md
"
`;

exports[`toc-loader > includes > should merge includes in flat link mode 1`] = `
"items:
- name: Inner Item 1
"
`;

exports[`toc-loader > includes > should merge includes in link mode 1`] = `
"items:
- name: Outer Item
items:
- name: Inner Item 1
"
`;

exports[`toc-loader > includes > should rebase items href for includes in link mode 1`] = `
"items:
- name: Outer Item
items:
- name: Inner Item 1
href: _includes/core/item-1.md
items:
- name: Inner Sub Item 1
href: _includes/core/sub-item-1.md
- name: Inner Item 2
href: _includes/core/item-2.md
- name: Inner Item 3
href: _includes/core/sub/item-3.md
- name: Inner Item 4
href: _includes/core/sub/item-4.md
- name: Inner Item 5
href: _includes/item-5.md
- name: Inner Item 6
href: https://example.com
- name: Inner Item 7
href: //example.com
"
`;

exports[`toc-loader > should filter hidden item 1`] = `
"items:
- name: Visible Item 1
- name: Visible Item 2
"
`;

exports[`toc-loader > should filter item with declined rule 1`] = `
"items:
- name: Visible Item 1
- name: Visible Item 2
"
`;

exports[`toc-loader > should handle filter title 1`] = `
"title: Title B
"
`;

exports[`toc-loader > should handle simple title 1`] = `
"title: Title
"
`;

exports[`toc-loader > should interpolate conditions in title 1`] = `
"title: 'Title IF '
"
`;

exports[`toc-loader > should interpolate filter title 1`] = `
"title: Title C
"
`;

exports[`toc-loader > should interpolate item href 1`] = `
"items:
- href: ./file.md
"
`;

exports[`toc-loader > should interpolate item name 1`] = `
"items:
- name: Item C
"
`;

exports[`toc-loader > should interpolate nested item 1`] = `
"items:
- name: Parent
items:
- name: Item C
href: ./file.md
"
`;

exports[`toc-loader > should interpolate title 1`] = `
"title: Title C
"
`;

exports[`toc-loader > should not filter item with accepted rule 1`] = `
"items:
- name: Visible Item 1
- name: Visible Item 2
- name: Visible Item 3
"
`;

exports[`toc-loader > should not interpolate title if conditions is disabled 1`] = `
"title: Title {% if var == "C"%} IF {% endif %}
"
`;

exports[`toc-loader > should not interpolate title if substitutions is disabled 1`] = `
"title: Title {{var}}
"
`;
39 changes: 39 additions & 0 deletions src/commands/build/core/toc/includers/generic.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import {when} from 'vitest-when';
import {join} from 'node:path';

import {setupBuild, setupRun} from '~/commands/build/__tests__';

import {GenericIncluderExtension} from './generic';

describe('Generic includer', () => {
it('should work', async () => {
const input = '/dev/null/input' as AbsolutePath;
const output = '/dev/null/output' as AbsolutePath;
const build = setupBuild();
const run = setupRun({input, output});
const extension = new GenericIncluderExtension();

when(run.glob).calledWith('**/*.md', {
cwd: join(run.input, 'test')
}).thenResolve([
'./index.md',
'./test.md',
'./sub/sub-1.md',
'./sub/sub-2.md',
'./sub/sub/sub-3.md',
'./skip/sub/sub-1.md',
] as RelativePath[]);

extension.apply(build);

await build.hooks.BeforeAnyRun.promise(run);

const result = await run.toc.hooks.Includer.for('generic').promise({}, {
input: 'test' as RelativePath,
path: 'test/toc.yaml' as RelativePath,
}, './toc.yaml' as RelativePath);

console.log(JSON.stringify(result, null, 2));

Check warning on line 37 in src/commands/build/core/toc/includers/generic.spec.ts

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
});
});
Loading

0 comments on commit f65e1b1

Please sign in to comment.