From 09f0781e8a133439b72491d618a2c73b0ba0e2f9 Mon Sep 17 00:00:00 2001 From: 3y3 <3y3@ya.ru> Date: Wed, 18 Dec 2024 20:18:01 +0300 Subject: [PATCH 1/4] feat: Implement Toc service --- src/.eslintrc.js | 6 + src/commands/build/core/toc/TocService.ts | 226 +++++++ .../core/toc/__snapshots__/index.spec.ts.snap | 192 ++++++ .../__snapshots__/generic.spec.ts.snap | 76 +++ .../build/core/toc/includers/generic.spec.ts | 119 ++++ .../build/core/toc/includers/generic.ts | 93 +++ .../build/core/toc/includers/openapi.ts | 27 + src/commands/build/core/toc/index.spec.ts | 602 ++++++++++++++++++ src/commands/build/core/toc/index.ts | 7 + src/commands/build/core/toc/loader.ts | 279 ++++++++ src/commands/build/core/toc/types.ts | 85 +++ src/commands/build/core/toc/utils.ts | 35 + src/commands/build/features/html/index.ts | 28 + .../build/features/singlepage/index.ts | 26 + .../build/features/singlepage/utils.ts | 39 ++ src/commands/build/handler.ts | 14 +- src/commands/build/index.ts | 20 + src/commands/build/run.ts | 7 +- src/commands/build/types.ts | 1 + src/globals.d.ts | 6 +- src/resolvers/md2html.ts | 10 +- src/resolvers/md2md.ts | 6 +- src/services/contributors.ts | 22 +- src/services/includers/batteries/common.ts | 46 -- src/services/includers/batteries/generic.ts | 150 ----- src/services/includers/batteries/index.ts | 3 - .../includers/batteries/sourcedocs.ts | 36 -- src/services/includers/batteries/unarchive.ts | 110 ---- src/services/includers/index.ts | 159 ----- src/services/index.ts | 1 - src/services/metadata/enrich.ts | 12 +- src/services/metadata/vcsMetadata.ts | 33 +- src/services/tocs.ts | 451 +------------ src/services/utils.ts | 20 - src/steps/index.ts | 1 - src/steps/processExcludedFiles.ts | 15 +- src/steps/processMapFile.ts | 15 +- src/steps/processPages.ts | 96 +-- src/steps/processServiceFiles.ts | 25 - src/utils/index.ts | 1 - src/utils/singlePage.ts | 32 +- src/utils/toc.ts | 71 --- src/utils/url.ts | 2 +- .../__snapshots__/include-toc.test.ts.snap | 45 -- .../load-custom-resources.spec.ts.snap | 6 +- tests/e2e/__snapshots__/metadata.spec.ts.snap | 2 +- tests/e2e/__snapshots__/rtl.spec.ts.snap | 6 +- .../services/metadataAuthors.test.ts | 19 +- .../services/metadataContributors.test.ts | 21 +- 49 files changed, 2008 insertions(+), 1296 deletions(-) create mode 100644 src/.eslintrc.js create mode 100644 src/commands/build/core/toc/TocService.ts create mode 100644 src/commands/build/core/toc/__snapshots__/index.spec.ts.snap create mode 100644 src/commands/build/core/toc/includers/__snapshots__/generic.spec.ts.snap create mode 100644 src/commands/build/core/toc/includers/generic.spec.ts create mode 100644 src/commands/build/core/toc/includers/generic.ts create mode 100644 src/commands/build/core/toc/includers/openapi.ts create mode 100644 src/commands/build/core/toc/index.spec.ts create mode 100644 src/commands/build/core/toc/index.ts create mode 100644 src/commands/build/core/toc/loader.ts create mode 100644 src/commands/build/core/toc/types.ts create mode 100644 src/commands/build/core/toc/utils.ts create mode 100644 src/commands/build/features/html/index.ts create mode 100644 src/commands/build/features/singlepage/utils.ts delete mode 100644 src/services/includers/batteries/common.ts delete mode 100644 src/services/includers/batteries/generic.ts delete mode 100644 src/services/includers/batteries/index.ts delete mode 100644 src/services/includers/batteries/sourcedocs.ts delete mode 100644 src/services/includers/batteries/unarchive.ts delete mode 100644 src/services/includers/index.ts delete mode 100644 src/steps/processServiceFiles.ts delete mode 100644 src/utils/toc.ts diff --git a/src/.eslintrc.js b/src/.eslintrc.js new file mode 100644 index 00000000..8a59239b --- /dev/null +++ b/src/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['../.eslintrc.js'], + rules: { + 'valid-jsdoc': 'off', + }, +}; diff --git a/src/commands/build/core/toc/TocService.ts b/src/commands/build/core/toc/TocService.ts new file mode 100644 index 00000000..e0aeab80 --- /dev/null +++ b/src/commands/build/core/toc/TocService.ts @@ -0,0 +1,226 @@ +import type {BuildConfig, Run} from '~/commands/build'; +import type {IncluderOptions, RawToc, RawTocItem, Toc, TocItem, WithItems} from './types'; + +import {ok} from 'node:assert'; +import {basename, dirname, join} from 'node:path'; +import {dump, load} from 'js-yaml'; +import {AsyncParallelHook, AsyncSeriesWaterfallHook, HookMap} from 'tapable'; + +import {freeze, intercept, isExternalHref, normalizePath, own} from '~/utils'; +import {Stage} from '~/constants'; + +import {IncludeMode, LoaderContext, loader} from './loader'; + +export type TocServiceConfig = { + ignore: BuildConfig['ignore']; + ignoreStage: BuildConfig['ignoreStage']; + template: BuildConfig['template']; + removeHiddenTocItems: BuildConfig['removeHiddenTocItems']; +}; + +type WalkStepResult = I | I[] | null | undefined; + +type TocServiceHooks = { + /** + * Called before item data processing (but after data interpolation) + */ + Item: AsyncSeriesWaterfallHook<[RawTocItem, RelativePath]>; + Includer: HookMap>; + Resolved: AsyncParallelHook<[Toc, RelativePath]>; + Included: AsyncParallelHook<[Toc, RelativePath, IncludeInfo]>; +}; + +type IncludeInfo = { + from: RelativePath; + mode: IncludeMode; + mergeBase?: RelativePath; + content?: RawToc; +}; + +export class TocService { + hooks: TocServiceHooks; + + get entries() { + return [...this._entries]; + } + + private run: Run; + + private logger: Run['logger']; + + private vars: Run['vars']; + + private config: TocServiceConfig; + + private tocs: Map = new Map(); + + private _entries: Set = new Set(); + + private processed: Hash = {}; + + 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', 'path', 'info']), + }); + } + + async init() { + const tocs = await this.run.glob('**/toc.yaml', { + cwd: this.run.input, + ignore: this.config.ignore, + }); + + for (const toc of tocs) { + await this.load(toc); + } + } + + async load(path: RelativePath, include?: IncludeInfo): Promise { + path = normalizePath(path); + + // There is no error. We really skip toc processing, if it was processed previously in any way. + // For example toc can be processed as include of some other toc. + if (!include && this.processed[path]) { + return; + } + + this.processed[path] = true; + + 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 = { + mode: include?.mode || IncludeMode.RootMerge, + linkBase: include?.from || dirname(path), + mergeBase: include?.mergeBase, + path, + vars: await this.vars.load(path), + toc: this, + options: { + resolveConditions: this.config.template.features.conditions, + resolveSubstitutions: this.config.template.features.substitutions, + removeHiddenItems: this.config.removeHiddenTocItems, + }, + }; + + const content = include?.content || (load(await this.run.read(file)) as RawToc); + + // Should ignore included toc with tech-preview stage. + // TODO(major): remove this + if (content && content.stage === Stage.TECH_PREVIEW) { + return; + } + + const {ignoreStage} = this.config; + if (content.stage && ignoreStage.length && ignoreStage.includes(content.stage)) { + return; + } + + 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, { + sourcePath: (file: string) => file.endsWith('.md'), + ignore: [basename(file), '**/toc.yaml'], + }); + } + + const toc = (await loader.call(context, content)) as Toc; + + // If this is a part of other toc.yaml + if (include) { + await this.hooks.Included.promise(toc, path, include); + } else { + // TODO: we don't need to store tocs in future + // All processing should subscribe on toc.hooks.Resolved + this.tocs.set(path as NormalizedPath, toc); + await this.walkItems([toc], (item: TocItem | Toc) => { + if (own(item, 'href') && !isExternalHref(item.href)) { + this._entries.add(normalizePath(join(dirname(path), item.href))); + } + + return item; + }); + + await this.hooks.Resolved.promise(freeze(toc), path); + } + + // eslint-disable-next-line consistent-return + return toc; + } + + dump(toc: Toc | undefined) { + ok(toc, 'Toc is empty.'); + + return dump(toc); + } + + /** + * Visits all passed items. Applies actor to each item. + * Then applies actor to each item in actor result.items. + * Returns actor results. + */ + async walkItems>( + items: T[] | undefined, + actor: (item: T) => Promise> | WalkStepResult, + ): Promise { + if (!items || !items.length) { + return items; + } + + const results: T[] = []; + const queue = [...items]; + while (queue.length) { + const item = queue.shift() as T; + + const result = await actor(item); + if (result) { + results.push(...([] as T[]).concat(result)); + } + } + + for (const result of results) { + if (own(result, 'items') && result.items?.length) { + result.items = await this.walkItems(result.items, actor); + } + } + + return results; + } + + /** + * Resolves toc path and data for any page path. + * Expects what all paths are already loaded in service. + */ + for(path: RelativePath): [RelativePath, Toc] { + path = normalizePath(path); + + if (!path) { + throw new Error('Error while finding toc dir.'); + } + + const tocPath = join(dirname(path), 'toc.yaml'); + + if (this.tocs.has(tocPath as NormalizedPath)) { + return [tocPath, this.tocs.get(tocPath as NormalizedPath) as Toc]; + } + + return this.for(dirname(path)); + } + + dir(path: RelativePath): RelativePath { + const [tocPath] = this.for(path); + + return dirname(tocPath); + } +} diff --git a/src/commands/build/core/toc/__snapshots__/index.spec.ts.snap b/src/commands/build/core/toc/__snapshots__/index.spec.ts.snap new file mode 100644 index 00000000..424a96fb --- /dev/null +++ b/src/commands/build/core/toc/__snapshots__/index.spec.ts.snap @@ -0,0 +1,192 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`toc-loader > includers > should fix include path 1`] = ` +"items: + - name: Common item +" +`; + +exports[`toc-loader > includers > should handle registered includer 1`] = ` +"items: + - name: Common item +" +`; + +exports[`toc-loader > includers > should merge includer toc to parent 1`] = ` +"items: + - name: Common item + - name: Includer item 1 +" +`; + +exports[`toc-loader > includers > should pass extra params to includer 1`] = ` +"items: + - name: Common item +" +`; + +exports[`toc-loader > includes > should filter include in preview stage 1`] = ` +"items: + - name: Common item +" +`; + +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 normalize items 1`] = ` +"items: + - name: Item without extension + href: some/href.md + - name: Item with slash + href: some/href/index.yaml +" +`; + +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 both templating is disabled 1`] = ` +"title: Title {% if var == "C"%} IF {% endif %} +" +`; + +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}} +" +`; diff --git a/src/commands/build/core/toc/includers/__snapshots__/generic.spec.ts.snap b/src/commands/build/core/toc/includers/__snapshots__/generic.spec.ts.snap new file mode 100644 index 00000000..a4fdf5fe --- /dev/null +++ b/src/commands/build/core/toc/includers/__snapshots__/generic.spec.ts.snap @@ -0,0 +1,76 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Generic includer > should use leadingPage.name option 1`] = ` +"items: + - name: Test Overview + href: index.md + - name: test + href: test.md + - name: sub + items: + - name: sub-1 + href: sub/sub-1.md + - name: sub-2 + href: sub/sub-2.md + - name: sub + items: + - name: sub-3 + href: sub/sub/sub-3.md + - name: skip + items: + - name: sub + items: + - name: sub-1 + href: skip/sub/sub-1.md +" +`; + +exports[`Generic includer > should use top path as input root, if input is not specified 1`] = ` +"items: + - name: Overview + href: index.md + - name: test + href: test.md + - name: sub + items: + - name: sub-1 + href: sub/sub-1.md + - name: sub-2 + href: sub/sub-2.md + - name: sub + items: + - name: sub-3 + href: sub/sub/sub-3.md + - name: skip + items: + - name: sub + items: + - name: sub-1 + href: skip/sub/sub-1.md +" +`; + +exports[`Generic includer > should work 1`] = ` +"items: + - name: Overview + href: index.md + - name: test + href: test.md + - name: sub + items: + - name: sub-1 + href: sub/sub-1.md + - name: sub-2 + href: sub/sub-2.md + - name: sub + items: + - name: sub-3 + href: sub/sub/sub-3.md + - name: skip + items: + - name: sub + items: + - name: sub-1 + href: skip/sub/sub-1.md +" +`; diff --git a/src/commands/build/core/toc/includers/generic.spec.ts b/src/commands/build/core/toc/includers/generic.spec.ts new file mode 100644 index 00000000..c81c2d31 --- /dev/null +++ b/src/commands/build/core/toc/includers/generic.spec.ts @@ -0,0 +1,119 @@ +import {describe, expect, it} from 'vitest'; +import {when} from 'vitest-when'; +import {join} from 'node:path'; +import {dump} from 'js-yaml'; + +import {setupBuild, setupRun} from '~/commands/build/__tests__'; + +import {GenericIncluderExtension} from './generic'; + +const prepareExtension = async (globs: [string, RelativePath, NormalizedPath[]][]) => { + const build = setupBuild(); + const run = setupRun({}); + const extension = new GenericIncluderExtension(); + + for (const [pattern, cwd, files] of globs) { + when(run.glob) + .calledWith( + pattern, + expect.objectContaining({ + cwd: join(run.input, cwd), + }), + ) + .thenResolve(files); + } + + extension.apply(build); + + await build.hooks.BeforeAnyRun.promise(run); + + return {build, run, extension}; +}; + +describe('Generic includer', () => { + it('should work', async () => { + const {run} = await prepareExtension([ + [ + '**/*.md', + './test', + [ + 'index.md', + 'test.md', + 'sub/sub-1.md', + 'sub/sub-2.md', + 'sub/sub/sub-3.md', + 'skip/sub/sub-1.md', + ] as NormalizedPath[], + ], + ]); + + const result = await run.toc.hooks.Includer.for('generic').promise( + {}, + { + input: './test', + path: './test/toc.yaml', + }, + './toc.yaml', + ); + + expect(dump(result)).toMatchSnapshot(); + }); + + it('should use top path as input root, if input is not specified', async () => { + const {run} = await prepareExtension([ + [ + '**/*.md', + './path/test', + [ + 'index.md', + 'test.md', + 'sub/sub-1.md', + 'sub/sub-2.md', + 'sub/sub/sub-3.md', + 'skip/sub/sub-1.md', + ] as NormalizedPath[], + ], + ]); + + const result = await run.toc.hooks.Includer.for('generic').promise( + {}, + { + path: './path/test/toc.yaml', + }, + './toc.yaml', + ); + + expect(dump(result)).toMatchSnapshot(); + }); + + it('should use leadingPage.name option', async () => { + const {run} = await prepareExtension([ + [ + '**/*.md', + './test', + [ + 'index.md', + 'test.md', + 'sub/sub-1.md', + 'sub/sub-2.md', + 'sub/sub/sub-3.md', + 'skip/sub/sub-1.md', + ] as NormalizedPath[], + ], + ]); + + const result = await run.toc.hooks.Includer.for('generic').promise( + {}, + { + input: './test', + path: './test/toc.yaml', + leadingPage: { + name: 'Test Overview', + }, + }, + './toc.yaml', + ); + + expect(dump(result)).toMatchSnapshot(); + }); +}); diff --git a/src/commands/build/core/toc/includers/generic.ts b/src/commands/build/core/toc/includers/generic.ts new file mode 100644 index 00000000..437e6823 --- /dev/null +++ b/src/commands/build/core/toc/includers/generic.ts @@ -0,0 +1,93 @@ +import type {Build} from '~/commands'; +import type {IncluderOptions, RawToc, RawTocItem, YfmString} from '~/commands/build'; + +import {dirname, extname, join} from 'node:path'; + +// const AUTOTITLE = '{$T}'; + +type Options = IncluderOptions<{ + input?: RelativePath; + autotitle?: boolean; + leadingPage?: { + autotitle?: boolean; + name?: string; + }; +}>; + +type Graph = { + [prop: string]: (YfmString & NormalizedPath) | Graph; +}; + +// TODO: implement autotitle after md refactoring +// TODO: implement sort +export class GenericIncluderExtension { + apply(program: Build) { + program.hooks.BeforeAnyRun.tap('GenericIncluder', (run) => { + run.toc.hooks.Includer.for('generic').tapPromise( + 'GenericIncluder', + async (toc, options: Options, path) => { + const input = options.input + ? join(dirname(path), options.input) + : dirname(options.path); + const files = await run.glob('**/*.md', { + cwd: join(run.input, input), + }); + + return fillToc(toc, graph(files), options); + }, + ); + }); + } +} + +function graph(paths: NormalizedPath[]): Graph { + const graph: Graph = {}; + + for (const path of paths) { + const chunks: string[] = path.split('/'); + + let level: Hash = graph; + while (chunks.length) { + const field = chunks.shift() as string; + + if (chunks.length) { + level[field] = level[field] || {}; + level = level[field]; + } else { + level[field.replace(extname(field), '')] = path; + } + } + } + + return graph; +} + +function pageName(key: string, options: Options) { + if (key === 'index') { + // if (options?.leadingPage?.autotitle) { + // return AUTOTITLE; + // } + + // TODO: i18n + return (options?.leadingPage?.name ?? 'Overview') as YfmString; + } + + // return options.autotitle ? AUTOTITLE : key; + return key as YfmString; +} + +function fillToc(toc: RawToc, graph: Graph, options: Options) { + function item([key, value]: [string, Graph | (NormalizedPath & YfmString)]): RawTocItem { + const name = pageName(key, options); + + if (typeof value === 'string') { + return {name, href: value}; + } + + return {name, items: Object.entries(value).map(item)}; + } + + toc.items = Object.entries(graph).map(item); + + return toc; +} diff --git a/src/commands/build/core/toc/includers/openapi.ts b/src/commands/build/core/toc/includers/openapi.ts new file mode 100644 index 00000000..de2b2a9e --- /dev/null +++ b/src/commands/build/core/toc/includers/openapi.ts @@ -0,0 +1,27 @@ +import type {Build, RawToc} from '~/commands/build'; + +import {dirname, join} from 'node:path'; +import {includer} from '@diplodoc/openapi-extension/includer'; + +// TODO: move to openapi-extension on major +export class OpenapiIncluderExtension { + apply(program: Build) { + program.hooks.BeforeAnyRun.tap('OpenapiIncluder', (run) => { + run.toc.hooks.Includer.for('openapi').tapPromise( + 'OpenapiIncluder', + async (_toc, options, path) => { + // @ts-ignore + const {toc, files} = await includer(run, options, path); + + const root = join(run.input, dirname(options.path)); + for (const {path, content} of files) { + await run.write(join(root, path), content); + } + + // @ts-ignore + return toc as RawToc; + }, + ); + }); + } +} diff --git a/src/commands/build/core/toc/index.spec.ts b/src/commands/build/core/toc/index.spec.ts new file mode 100644 index 00000000..88c48807 --- /dev/null +++ b/src/commands/build/core/toc/index.spec.ts @@ -0,0 +1,602 @@ +import type {RunSpy} from '~/commands/build/__tests__'; +import type {Toc} from './types'; + +import {join} from 'node:path'; +import {describe, expect, it, vi} from 'vitest'; +import {when} from 'vitest-when'; +import {dedent} from 'ts-dedent'; + +import {TocService, TocServiceConfig} from './TocService'; +import {setupRun} from '~/commands/build/__tests__'; + +type Options = DeepPartial; + +vi.mock('../vars/VarsService'); + +function setupService(options: Options = {}) { + const run = setupRun({ + ignoreStage: [], + removeHiddenTocItems: false, + ...options, + template: { + enabled: true, + ...(options.template || {}), + features: { + conditions: true, + substitutions: true, + ...((options.template || {}).features || {}), + }, + }, + }); + const toc = new TocService(run); + + return {run, toc}; +} + +function mockData(run: RunSpy, content: string, vars: Vars, files: Files, copy: Copy) { + when(run.vars.load) + .calledWith('toc.yaml' as NormalizedPath) + .thenResolve(vars); + + when(run.read).calledWith(join(run.input, './toc.yaml')).thenResolve(content); + + for (const [path, content] of Object.entries(files)) { + when(run.read) + .calledWith(join(run.input, path)) + .thenResolve(content as string); + } + + for (const [from, to] of copy) { + when(run.copy) + .calledWith(join(run.input, from), join(run.input, to), expect.anything()) + .thenResolve(); + } +} + +type Vars = Hash; +type Files = Hash; +type Copy = [RelativePath, RelativePath][]; +function test( + content: string, + options: Options = {}, + vars: Vars = {}, + files: Files = {}, + copy: Copy = [], +) { + return async () => { + const {run, toc} = setupService(options); + + mockData(run, content, vars, files, copy); + + const result = (await toc.load('toc.yaml' as NormalizedPath)) as Toc; + + expect(toc.dump(result)).toMatchSnapshot(); + }; +} + +describe('toc-loader', () => { + it( + 'should handle simple title', + test(dedent` + title: Title + `), + ); + + it( + 'should handle filter title', + test( + dedent` + title: + - text: Title A + when: var == "A" + - text: Title B + when: var == "B" + `, + {}, + {var: 'B'}, + ), + ); + + it('should interpolate title', test(`title: Title {{var}}`, {}, {var: 'C'})); + + it( + 'should interpolate conditions in title', + test( + dedent` + title: Title {% if var == "C"%} IF {% endif %} + `, + {}, + {var: 'C'}, + ), + ); + + it( + 'should interpolate filter title', + test( + dedent` + title: + - text: Title A + when: var == "A" + - text: Title B + when: var == "B" + - text: Title {{var}} + when: var == "C" + `, + {}, + {var: 'C'}, + ), + ); + + it( + 'should not interpolate title if substitutions is disabled', + test( + dedent` + title: Title {{var}} + `, + {template: {features: {substitutions: false}}}, + {var: 'C'}, + ), + ); + + it( + 'should not interpolate title if conditions is disabled', + test( + dedent` + title: Title {% if var == "C"%} IF {% endif %} + `, + {template: {features: {conditions: false}}}, + {var: 'C'}, + ), + ); + + it( + 'should not interpolate title if both templating is disabled', + test( + dedent` + title: Title {% if var == "C"%} IF {% endif %} + `, + {template: {features: {conditions: false, substitutions: false}}}, + {var: 'C'}, + ), + ); + + it( + 'should not filter item with accepted rule', + test( + dedent` + items: + - name: Visible Item 1 + - name: Visible Item {{name}} + when: stage == 'test' + - name: Visible Item 3 + `, + {}, + {stage: 'test', name: 2}, + ), + ); + + it( + 'should filter item with declined rule', + test( + dedent` + items: + - name: Visible Item 1 + - name: Item {{name}} + when: stage == 'test' + - name: Visible Item 2 + `, + {}, + {stage: 'dev'}, + ), + ); + + it( + 'should filter hidden item', + test( + dedent` + items: + - name: Visible Item 1 + - name: Hidden Item {{name}} + hidden: true + when: stage == 'test' + - name: Visible Item 2 + when: stage == 'test' + `, + {removeHiddenTocItems: true}, + {stage: 'test'}, + ), + ); + + it( + 'should interpolate item name', + test( + dedent` + items: + - name: Item {{name}} + `, + {}, + {name: 'C'}, + ), + ); + + it( + 'should interpolate item href', + test( + dedent` + items: + - href: "{{file}}" + `, + {}, + {file: './file.md'}, + ), + ); + + it( + 'should interpolate nested item', + test( + dedent` + items: + - name: Parent + items: + - name: Item {{name}} + href: "{{file}}" + `, + {}, + {name: 'C', file: './file.md'}, + ), + ); + + it( + 'should normalize items', + test( + dedent` + items: + - name: Item without extension + href: some/href + - name: Item with slash + href: some/href/ + `, + {}, + {}, + ), + ); + + describe('includes', () => { + it( + 'should rebase items href for includes in link mode', + test( + dedent` + items: + - name: Outer Item + include: + path: _includes/core/i-toc.yaml + mode: link + `, + {}, + {}, + { + '_includes/core/i-toc.yaml': dedent` + items: + - name: Inner Item 1 + href: item-1.md + items: + - name: Inner Sub Item 1 + href: sub-item-1.md + - name: Inner Item 2 + href: ./item-2.md + - name: Inner Item 3 + href: ./sub/item-3.md + - name: Inner Item 4 + href: sub/item-4.md + - name: Inner Item 5 + href: ../item-5.md + - name: Inner Item 6 + href: https://example.com + - name: Inner Item 7 + href: //example.com + `, + }, + ), + ); + + it( + 'should filter include in preview stage', + test( + dedent` + items: + - name: Common item + - name: Filtered item + include: + path: _includes/core/i-toc.yaml + mode: link + `, + {}, + {}, + { + '_includes/core/i-toc.yaml': dedent` + stage: tech-preview + items: + - name: Inner Item 1 + `, + }, + ), + ); + + it( + 'should merge includes in link mode', + test( + dedent` + items: + - name: Outer Item + include: + path: _includes/core/i-toc.yaml + mode: link + `, + {}, + {}, + { + '_includes/core/i-toc.yaml': dedent` + items: + - name: Inner Item 1 + `, + }, + ), + ); + + it( + 'should merge includes in flat link mode', + test( + dedent` + items: + - include: + path: _includes/core/i-toc.yaml + mode: link + `, + {}, + {}, + { + '_includes/core/i-toc.yaml': dedent` + items: + - name: Inner Item 1 + `, + }, + ), + ); + + it( + 'should merge deep includes in link mode', + test( + dedent` + items: + - name: Outer Item + include: + path: _includes/core/i-toc.yaml + mode: link + `, + {}, + {}, + { + '_includes/core/i-toc.yaml': dedent` + items: + - name: Inner Item 1 + href: item-1.md + - name: Inner Item 2 + include: + path: ../lib/i-toc.yaml + mode: link + `, + '_includes/lib/i-toc.yaml': dedent` + items: + - name: Inner Lib Item 1 + href: item-1.md + `, + }, + ), + ); + + it( + 'should merge deep includes in merge mode', + test( + dedent` + items: + - name: Outer Item + include: + path: _includes/core/toc.yaml + mode: link + `, + {}, + {}, + { + '_includes/core/toc.yaml': dedent` + items: + - name: Inner Item 1 + href: item-1.md + - name: Inner Item 2 + include: + path: ../merge/i-toc.yaml + mode: merge + `, + '_includes/core/sub/toc.yaml': dedent` + items: + - name: Inner Sub Item 1 + href: sub-item-1.md + `, + '_includes/merge/i-toc.yaml': dedent` + items: + - name: Inner Merge Item 1 + href: merge-item-1.md + - include: + path: ../deep-merge/i-toc.yaml + mode: merge + - include: + path: ./sub/toc.yaml + mode: merge + `, + '_includes/deep-merge/i-toc.yaml': dedent` + items: + - name: Inner Deep Merge Item 1 + href: deep-merge-item-1.md + `, + }, + [ + ['_includes/merge', '_includes/core'], + ['_includes/deep-merge', '_includes/core'], + ['_includes/core/sub', '_includes/core'], + ] as [RelativePath, RelativePath][], + ), + ); + }); + + describe('includers', () => { + it('should throw on unregistered includer', async () => { + await expect( + test( + dedent` + items: + - name: Common item + - include: + path: _includes/core/i-toc.yaml + mode: link + includers: + - name: unknown + `, + {}, + {}, + { + '_includes/core/i-toc.yaml': dedent` + items: + - name: Inner Item 1 + `, + }, + ), + ).rejects.toThrow(`Includer with name 'unknown' is not registered.`); + }); + + it('should handle registered includer', async () => { + const {run, toc} = setupService({}); + const content = dedent` + items: + - name: Common item + - include: + path: _includes/core/i-toc.yaml + mode: link + includers: + - name: expected + `; + const files = { + '_includes/core/i-toc.yaml': dedent` + items: + - name: Inner Item 1 + `, + }; + + mockData(run, content, {}, files, []); + + toc.hooks.Includer.for('expected').tap('Tests', (toc) => ({ + ...toc, + stage: 'test', + })); + + const result = (await toc.load('toc.yaml' as NormalizedPath)) as Toc; + + expect(toc.dump(result)).toMatchSnapshot(); + }); + + it('should fix include path', async () => { + expect.assertions(2); + + const {run, toc} = setupService({}); + const content = dedent` + items: + - name: Common item + - include: + path: _includes/core + mode: link + includers: + - name: expected + `; + const files = {}; + + mockData(run, content, {}, files, []); + + toc.hooks.Includer.for('expected').tap('Tests', (toc, options) => { + expect(options).toMatchObject({ + path: '_includes/core/toc.yaml', + }); + + return { + ...toc, + stage: 'test', + }; + }); + + const result = (await toc.load('toc.yaml' as NormalizedPath)) as Toc; + + expect(toc.dump(result)).toMatchSnapshot(); + }); + + it('should pass extra params to includer', async () => { + expect.assertions(2); + + const {run, toc} = setupService({}); + const content = dedent` + items: + - name: Common item + - include: + path: _includes/core + mode: link + includers: + - name: expected + field: value + `; + const files = {}; + + mockData(run, content, {}, files, []); + + toc.hooks.Includer.for('expected').tap('Tests', (toc, options) => { + expect(options).toMatchObject({ + path: '_includes/core/toc.yaml', + field: 'value', + }); + + return { + ...toc, + stage: 'test', + }; + }); + + const result = (await toc.load('toc.yaml' as NormalizedPath)) as Toc; + + expect(toc.dump(result)).toMatchSnapshot(); + }); + + it('should merge includer toc to parent', async () => { + const {run, toc} = setupService({}); + const content = dedent` + items: + - name: Common item + - include: + path: _includes/core + mode: link + includers: + - name: expected + field: value + `; + const files = {}; + + mockData(run, content, {}, files, []); + + toc.hooks.Includer.for('expected').tap('Tests', (toc) => { + return { + ...toc, + stage: 'test', + items: [{name: 'Includer item 1'}], + }; + }); + + const result = (await toc.load('toc.yaml' as NormalizedPath)) as Toc; + + expect(toc.dump(result)).toMatchSnapshot(); + }); + }); +}); diff --git a/src/commands/build/core/toc/index.ts b/src/commands/build/core/toc/index.ts new file mode 100644 index 00000000..85968984 --- /dev/null +++ b/src/commands/build/core/toc/index.ts @@ -0,0 +1,7 @@ +export type * from './types'; +export type {LoaderContext} from './loader'; + +export {IncludeMode} from './loader'; +export {TocService} from './TocService'; +export {GenericIncluderExtension} from './includers/generic'; +export {OpenapiIncluderExtension} from './includers/openapi'; diff --git a/src/commands/build/core/toc/loader.ts b/src/commands/build/core/toc/loader.ts new file mode 100644 index 00000000..e34d0d06 --- /dev/null +++ b/src/commands/build/core/toc/loader.ts @@ -0,0 +1,279 @@ +import type {TocService} from './TocService'; +import type {RawToc, RawTocItem, YfmString} from './types'; + +import {ok} from 'node:assert'; +import {dirname, join, relative} from 'node:path'; +import {omit} from 'lodash'; +import evalExp from '@diplodoc/transform/lib/liquid/evaluation'; +import {liquidSnippet} from '@diplodoc/transform/lib/liquid'; + +import {isExternalHref, normalizePath, own} from '~/utils'; +import {getFirstValuable, isRelative} from './utils'; + +export type LoaderContext = { + /** Relative to run.input path to current processing toc */ + path: RelativePath; + /** Path of last include level */ + linkBase: RelativePath; + /** Path of last include level with 'merge' mode */ + mergeBase?: RelativePath; + mode: IncludeMode; + vars: Hash; + options: { + resolveConditions: boolean; + resolveSubstitutions: boolean; + removeHiddenItems: boolean; + }; + toc: TocService; +}; + +export enum IncludeMode { + RootMerge = 'root_merge', + Merge = 'merge', + Link = 'link', +} + +// Designed to be isolated loaders in future +export async function loader(this: LoaderContext, toc: RawToc): Promise { + // Resolves toc fields which can be filterable arrays. + // Apply when filter in some toc fields + toc = await resolveFields.call(this, toc); + + // Apply when filter in toc.items + // Drop hidden items + toc = await resolveItems.call(this, toc); + + // Interpolate liquid vars in some toc fields + toc = await templateFields.call(this, toc); + + // Resolve includes and includers in toc items + toc = await processItems.call(this, toc); + + // Rebase items href path for deep includes + toc = await rebaseItems.call(this, toc); + + // Fix item href extensions + toc = await normalizeItems.call(this, toc); + + return toc; +} + +/** + * Resolves toc fields which can be filterable arrays. + * Convert arrays to text fields (gets first truth value) + */ +async function resolveFields(this: LoaderContext, toc: RawToc): Promise { + for (const field of ['title', 'label'] as const) { + const value = toc[field]; + if (value) { + toc[field] = getFirstValuable(value, this.vars); + } + } + + return toc; +} + +/** + * Applies liquid substitutions for some toc fields + */ +async function templateFields(this: LoaderContext, toc: RawToc): Promise { + const {resolveConditions, resolveSubstitutions} = this.options; + const interpolate = (box: Hash, field: string) => { + const value = box[field]; + if (typeof value !== 'string') { + return; + } + + box[field] = liquidSnippet(value, this.vars, this.path, { + substitutions: resolveSubstitutions, + conditions: resolveConditions, + keepNotVar: true, + withSourceMap: false, + }); + }; + + if (!resolveConditions && !resolveSubstitutions) { + return toc; + } + + for (const field of ['href', 'title', 'label', 'navigation'] as const) { + interpolate(toc, field); + } + + toc.items = await this.toc.walkItems(toc.items, (item: RawTocItem) => { + for (const field of ['name', 'href'] as const) { + interpolate(item, field); + } + + return item; + }); + + return toc; +} + +/** + * Applies `when` filter in toc items. + * Also drops hidden items if needed. + */ +async function resolveItems(this: LoaderContext, toc: RawToc): Promise { + const {removeHiddenItems, resolveConditions} = this.options; + + if (!removeHiddenItems && !resolveConditions) { + return toc; + } + + toc.items = await this.toc.walkItems(toc.items, (item: RawTocItem) => { + let when = true; + + if (resolveConditions) { + when = + typeof item.when === 'string' ? evalExp(item.when, this.vars) : item.when !== false; + delete item.when; + } + + if (removeHiddenItems) { + when = when && !item.hidden; + delete item.hidden; + } + + return when ? item : undefined; + }); + + return toc; +} + +/** + * Processes items includes and includers. + * Then merges result in original place in `named` or `inline` mode. + */ +async function processItems(this: LoaderContext, toc: RawToc): Promise { + toc.items = await this.toc.walkItems(toc.items, async (item) => { + item = await this.toc.hooks.Item.promise(item, this.path); + + if (!item || !own(item, 'include')) { + return item; + } + + const {include} = item; + + ok(include.path, 'Invalid value for include path.'); + + let toc: RawToc | undefined = {}; + if (own(include, 'includers')) { + ok( + include.mode === IncludeMode.Link || !include.mode, + 'Invalid mode value for include with includers.', + ); + ok(Array.isArray(include.includers), 'Includers should be an array.'); + + const path = include.path.endsWith('toc.yaml') + ? include.path + : join(include.path, 'toc.yaml'); + const tocPath = join(dirname(this.path), path); + + for (const includer of include.includers) { + const hook = this.toc.hooks.Includer.get(includer.name); + + ok(includer.name, 'Includer name should be a string.'); + ok(hook, `Includer with name '${includer.name}' is not registered.`); + + const options = { + ...includer, + path: tocPath, + }; + + toc = await hook.promise(toc, options, this.path); + } + + toc = (await this.toc.load(tocPath, { + from: this.path, + mode: IncludeMode.Link, + mergeBase: this.mergeBase, + content: toc, + })) as RawToc; + } else { + const includeInfo = { + from: this.path, + path: join(dirname(this.path), include.path), + mode: include.mode || IncludeMode.RootMerge, + mergeBase: this.mergeBase, + }; + + if ([IncludeMode.RootMerge, IncludeMode.Merge].includes(includeInfo.mode)) { + includeInfo.mergeBase = includeInfo.mergeBase || dirname(this.path); + includeInfo.path = join(includeInfo.mergeBase, include.path); + } + + toc = (await this.toc.load(includeInfo.path, includeInfo)) as RawToc; + } + + item = omit(item, ['include']) as RawTocItem; + + if (!toc) { + return null; + } + + // named mode + if (item.name) { + item.items = (item.items || []).concat((toc.items as RawTocItem[]) || []); + + return item; + } else { + return toc.items as RawTocItem[]; + } + }); + + return toc; +} + +/** + * Rebuses items href after include in parent toc + */ +async function rebaseItems(this: LoaderContext, toc: RawToc): Promise { + if (this.mode !== IncludeMode.Link) { + return toc; + } + + const rebase = (item: RawTocItem | RawToc) => { + if (own(item, 'href') && isRelative(item.href)) { + const absBase = join(dirname(this.linkBase) as RelativePath); + const absPath = join(this.mergeBase || dirname(this.path), item.href); + + item.href = relative(absBase, absPath); + } + + return item; + }; + + await this.toc.walkItems([toc], rebase); + + return toc; +} + +/** + * Fixes item href extensions + */ +async function normalizeItems(this: LoaderContext, toc: RawToc): Promise { + await this.toc.walkItems([toc], (item: RawTocItem | RawToc) => { + // Looks like this logic is useless + // because we override ids on client + // + // (item as Partial).id = uuid(); + + if (own(item, 'href') && !isExternalHref(item.href)) { + item.href = normalizePath(item.href); + + if (item.href.endsWith('/')) { + item.href = `${item.href}index.yaml`; + } + + if (!item.href.endsWith('.md') && !item.href.endsWith('.yaml')) { + item.href = `${item.href}.md`; + } + } + + return item; + }); + + return toc; +} diff --git a/src/commands/build/core/toc/types.ts b/src/commands/build/core/toc/types.ts new file mode 100644 index 00000000..12935a9e --- /dev/null +++ b/src/commands/build/core/toc/types.ts @@ -0,0 +1,85 @@ +import type {IncludeMode} from './loader'; + +export type YfmString = string & { + __interpolable: true; +}; + +export type Filter = { + when?: string | boolean; +}; + +export type TextFilter = { + text: string; +} & Filter; + +export type WithItems = { + items?: Item[]; +}; + +export type RawToc = { + title?: YfmString | TextFilter[]; + label?: YfmString | TextFilter[]; + stage?: string; + href?: YfmString & (RelativePath | URIString); + navigation?: boolean | YfmString | Navigation; + items?: RawTocItem[]; +}; + +// TODO: add precise types +export type Navigation = { + logo: object; + header: { + leftItems?: object; + rightItems?: object; + }; +}; + +export type RawTocItem = Filter & { + hidden?: boolean; + items?: RawTocItem[]; +} & (RawNamedTocItem | RawIncludeTocItem); + +type RawNamedTocItem = { + name: YfmString; + href?: YfmString & (RelativePath | URIString); +}; + +type RawIncludeTocItem = { + name?: YfmString; + include: TocInclude; +}; + +type TocInclude = { + mode?: IncludeMode; + path: RelativePath; + includers?: Includer[]; +}; + +export type Includer = { + name: string; +} & T; + +export type IncluderOptions = { + /** + * Relative to run.input include path + */ + path: RelativePath; +} & T; + +export type Toc = { + title?: string; + label?: string; + stage?: string; + href?: string & NormalizedPath; + navigation?: boolean | Navigation; + items?: TocItem[]; +}; + +export type TocItem = NamedTocItem & {hidden?: boolean} & { + items?: TocItem[]; +}; + +export type NamedTocItem = { + name: string; + href?: NormalizedPath; +}; diff --git a/src/commands/build/core/toc/utils.ts b/src/commands/build/core/toc/utils.ts new file mode 100644 index 00000000..9d01139e --- /dev/null +++ b/src/commands/build/core/toc/utils.ts @@ -0,0 +1,35 @@ +import type {TextFilter} from '~/commands/build/core/toc/types'; +import evalExp from '@diplodoc/transform/lib/liquid/evaluation'; + +export function isRelative(path: AnyPath): path is RelativePath { + return /^\.{1,2}\//.test(path) || !/^(\w{0,7}:)?\/\//.test(path); +} + +export function getFirstValuable( + items: TextFilter[] | string, + vars: Hash, + fallback?: T, +): T | undefined { + if (typeof items === 'string') { + items = [{text: items, when: true}]; + } + + if (!Array.isArray(items)) { + items = []; + } + + for (const item of items) { + let {when = true} = item; + delete item.when; + + if (typeof when === 'string') { + when = evalExp(when, vars); + } + + if (when) { + return item.text as T; + } + } + + return fallback; +} diff --git a/src/commands/build/features/html/index.ts b/src/commands/build/features/html/index.ts new file mode 100644 index 00000000..a72faa00 --- /dev/null +++ b/src/commands/build/features/html/index.ts @@ -0,0 +1,28 @@ +import type {Build, Toc, TocItem} from '../..'; + +import {basename, dirname, extname, join} from 'node:path'; +import {isExternalHref, own} from '~/utils'; + +export class Html { + apply(program: Build) { + program.hooks.BeforeRun.for('html').tap('Html', async (run) => { + run.toc.hooks.Resolved.tapPromise('Html', async (toc, path) => { + const copy = JSON.parse(JSON.stringify(toc)) as Toc; + await run.toc.walkItems([copy], (item: Toc | TocItem) => { + if (own(item, 'href') && !isExternalHref(item.href)) { + const fileExtension: string = extname(item.href); + const filename: string = basename(item.href, fileExtension) + '.html'; + + item.href = join(dirname(path), dirname(item.href), filename); + } + + return item; + }); + + const file = join(run.output, dirname(path), 'toc.js'); + + await run.write(file, `window.__DATA__.data.toc = ${JSON.stringify(copy)};`); + }); + }); + } +} diff --git a/src/commands/build/features/singlepage/index.ts b/src/commands/build/features/singlepage/index.ts index be266f63..658dcd73 100644 --- a/src/commands/build/features/singlepage/index.ts +++ b/src/commands/build/features/singlepage/index.ts @@ -1,7 +1,12 @@ import type {Build} from '~/commands'; import type {Command} from '~/config'; +import type {Toc} from '~/commands/build'; + +import {dirname, join} from 'node:path'; import {defined} from '~/config'; import {options} from './config'; +import {isExternalHref, own} from '~/utils'; +import {getSinglePageUrl} from './utils'; export type SinglePageArgs = { singlePage: boolean; @@ -22,5 +27,26 @@ export class SinglePage { return config; }); + + program.hooks.BeforeRun.for('html').tap('SinglePage', (run) => { + if (!run.config.singlePage) { + return; + } + + run.toc.hooks.Resolved.tapPromise('SinglePage', async (toc, path) => { + const copy = JSON.parse(JSON.stringify(toc)) as Toc; + await run.toc.walkItems([copy], (item) => { + if (own(item, 'href') && !isExternalHref(item.href)) { + item.href = getSinglePageUrl(dirname(path), item.href); + } + + return item; + }); + + const file = join(run.output, dirname(path), 'single-page-toc.js'); + + await run.write(file, `window.__DATA__.data.toc = ${JSON.stringify(copy)};`); + }); + }); } } diff --git a/src/commands/build/features/singlepage/utils.ts b/src/commands/build/features/singlepage/utils.ts new file mode 100644 index 00000000..c2aabb1b --- /dev/null +++ b/src/commands/build/features/singlepage/utils.ts @@ -0,0 +1,39 @@ +function dropExt(path: string) { + return path.replace(/\.(md|ya?ml|html)$/i, ''); +} + +// TODO: check that this is useless +function toUrl(path: string) { + // replace windows backslashes + return path.replace(/\\/g, '/').replace(/^\.\//, ''); +} + +function getAnchorId(tocDir: string, path: string) { + const [pathname, hash] = path.split('#'); + const url = toUrl(dropExt(pathname)) + (hash ? '#' + hash : ''); + + // TODO: encodeURIComponent will be best option + return relativeTo(tocDir, url.replace(/\.\.\/|[/#]/g, '_')); +} + +function relativeTo(root: string, path: string) { + root = toUrl(root); + path = toUrl(path); + + if (root && path.startsWith(root + '/')) { + path = path.replace(root + '/', ''); + } + + return path; +} + +export function getSinglePageUrl(tocDir: string, path: string) { + const prefix = toUrl(tocDir) || '.'; + const suffix = getAnchorId(tocDir, path); + + if (prefix === '.') { + return '#' + suffix; + } + + return prefix + '/single-page.html#' + suffix; +} diff --git a/src/commands/build/handler.ts b/src/commands/build/handler.ts index 90dfe68a..275fc6eb 100644 --- a/src/commands/build/handler.ts +++ b/src/commands/build/handler.ts @@ -2,12 +2,9 @@ import type {Run} from './run'; import 'threads/register'; -import OpenapiIncluder from '@diplodoc/openapi-extension/includer'; - -import {ArgvService, Includers, PresetService, SearchService} from '~/services'; +import {ArgvService, PresetService, SearchService, TocService} from '~/services'; import { initLinterWorkers, - preparingTocFiles, processAssets, processChangelogs, processExcludedFiles, @@ -21,18 +18,15 @@ export async function handler(run: Run) { try { ArgvService.init(run.legacyConfig); SearchService.init(); - // TODO: Remove duplicated types from openapi-extension - // @ts-ignore - Includers.init([OpenapiIncluder]); + PresetService.init(run.vars); + TocService.init(run.toc); const {lintDisabled, buildDisabled, addMapFile} = ArgvService.getConfig(); - PresetService.init(run.vars); - await preparingTocFiles(); processExcludedFiles(); if (addMapFile) { - prepareMapFile(); + prepareMapFile(run); } if (!lintDisabled) { diff --git a/src/commands/build/index.ts b/src/commands/build/index.ts index dec4df1d..2076636a 100644 --- a/src/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -2,6 +2,7 @@ import type {IProgram, ProgramArgs, ProgramConfig} from '~/program'; import type {DocAnalytics} from '@diplodoc/client'; import {ok} from 'node:assert'; +import {join} from 'node:path'; import {pick} from 'lodash'; import {AsyncParallelHook, AsyncSeriesHook, HookMap} from 'tapable'; @@ -23,8 +24,12 @@ import {SinglePage, SinglePageArgs, SinglePageConfig} from './features/singlepag import {Redirects} from './features/redirects'; import {Lint, LintArgs, LintConfig, LintRawConfig} from './features/linter'; import {Changelogs, ChangelogsArgs, ChangelogsConfig} from './features/changelogs'; +import {Html} from './features/html'; import {Search, SearchArgs, SearchConfig, SearchRawConfig} from './features/search'; import {Legacy, LegacyArgs, LegacyConfig, LegacyRawConfig} from './features/legacy'; + +import {GenericIncluderExtension, OpenapiIncluderExtension} from './core/toc'; + import shell from 'shelljs'; import {intercept} from '~/utils'; @@ -192,6 +197,8 @@ export class Build readonly changelogs = new Changelogs(); + readonly html = new Html(); + readonly search = new Search(); readonly legacy = new Legacy(); @@ -254,7 +261,14 @@ export class Build return config; }); + this.hooks.BeforeRun.for('md').tap('Build', (run) => { + run.toc.hooks.Resolved.tapPromise('Build', async (toc, path) => { + await run.write(join(run.output, path), run.toc.dump(toc)); + }); + }); + this.hooks.AfterRun.for('md').tap('Build', async (run) => { + // TODO: save normalized config instead if (run.config[configPath]) { shell.cp(run.config[configPath], run.output); } @@ -267,13 +281,18 @@ export class Build this.linter.apply(this); this.changelogs.apply(this); this.search.apply(this); + this.html.apply(this); this.legacy.apply(this); + new GenericIncluderExtension().apply(this); + new OpenapiIncluderExtension().apply(this); + super.apply(program); } async action() { if (typeof VERSION !== 'undefined' && process.env.NODE_ENV !== 'test') { + // eslint-disable-next-line no-console console.log(`Using v${VERSION} version`); } @@ -291,6 +310,7 @@ export class Build await run.copy(run.originalInput, run.input, ['node_modules/**', '*/node_modules/**']); await run.vars.init(); + await run.toc.init(); await Promise.all([handler(run), this.hooks.Run.promise(run)]); diff --git a/src/commands/build/run.ts b/src/commands/build/run.ts index 25d1be57..ada4fb40 100644 --- a/src/commands/build/run.ts +++ b/src/commands/build/run.ts @@ -19,6 +19,7 @@ import {LogLevel, Logger} from '~/logger'; import {legacyConfig} from './legacy-config'; import {InsecureAccessError} from './errors'; import {VarsService} from './core/vars'; +import {TocService} from './core/toc'; import {addSourcePath} from './core/meta'; type FileSystem = { @@ -70,6 +71,8 @@ export class Run { readonly vars: VarsService; + readonly toc: TocService; + get bundlePath() { return join(this.output, BUNDLE_FOLDER); } @@ -99,6 +102,8 @@ export class Run { ]); this.vars = new VarsService(this); + this.toc = new TocService(this); + this.legacyConfig = legacyConfig(this); } @@ -206,7 +211,7 @@ export class Run { this.logger.copy(join(from, file), join(to, file)); - if (sourcePath) { + if (sourcePath && sourcePath(file)) { const content = await this.read(join(from, file)); this.write( join(to, file), diff --git a/src/commands/build/types.ts b/src/commands/build/types.ts index cd567f4a..b540ba76 100644 --- a/src/commands/build/types.ts +++ b/src/commands/build/types.ts @@ -1 +1,2 @@ export type * from './core/vars'; +export type * from './core/toc'; diff --git a/src/globals.d.ts b/src/globals.d.ts index 3ea9d4c7..f768f68b 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -35,14 +35,16 @@ type RelativePath = string & */ type NormalizedPath = string & { __type: 'path'; - __mode: 'relative' & 'normalized'; + __mode: 'relative'; + __fix: 'normalized'; }; type AnyPath = string | UnresolvedPath | AbsolutePath | RelativePath | NormalizedPath; type URIString = string & { __type: 'uri'; - __mode: 'normalized'; + __mode: 'relative'; + __fix: 'normalized'; }; declare module 'node:path' { diff --git a/src/resolvers/md2html.ts b/src/resolvers/md2html.ts index d32ad3db..d82a1fba 100644 --- a/src/resolvers/md2html.ts +++ b/src/resolvers/md2html.ts @@ -1,4 +1,5 @@ import type {DocInnerProps} from '@diplodoc/client'; +import type {Run} from '~/commands/build'; import {readFileSync, writeFileSync} from 'fs'; import {dirname, extname, join, resolve} from 'path'; @@ -13,8 +14,8 @@ import {getPublicPath, isFileExists} from '@diplodoc/transform/lib/utilsFS'; import yaml from 'js-yaml'; import {Lang, PROCESSING_FINISHED} from '../constants'; -import {LeadingPage, ResolverOptions, YfmToc} from '../models'; -import {ArgvService, PluginService, SearchService, TocService} from '../services'; +import {LeadingPage, ResolverOptions} from '../models'; +import {ArgvService, PluginService, SearchService} from '../services'; import {getVCSMetadata} from '../services/metadata'; import { getDepth, @@ -104,11 +105,12 @@ const getFileProps = async (options: ResolverOptions) => { }; }; -export async function resolveMd2HTML(options: ResolverOptions): Promise { +export async function resolveMd2HTML(run: Run, options: ResolverOptions): Promise { const {outputPath, inputPath} = options; const props = await getFileProps(options); - const [tocDir, toc] = TocService.getForPath(inputPath) as [string, YfmToc]; + const [tocPath, toc] = run.toc.for(inputPath); + const tocDir = dirname(tocPath); const title = getTitle(toc.title as string, props.data.title); const tocInfo = { diff --git a/src/resolvers/md2md.ts b/src/resolvers/md2md.ts index 300913f3..33ba305b 100644 --- a/src/resolvers/md2md.ts +++ b/src/resolvers/md2md.ts @@ -1,3 +1,5 @@ +import type {Run} from '~/commands/build'; + import {readFileSync, writeFileSync} from 'fs'; import {basename, dirname, extname, join, resolve} from 'path'; import shell from 'shelljs'; @@ -11,14 +13,14 @@ import {PROCESSING_FINISHED} from '../constants'; import {ChangelogItem} from '@diplodoc/transform/lib/plugins/changelog/types'; import {enrichWithFrontMatter} from '../services/metadata'; -export async function resolveMd2Md(options: ResolveMd2MdOptions): Promise { +export async function resolveMd2Md(run: Run, options: ResolveMd2MdOptions): Promise { const {inputPath, outputPath, metadata: metadataOptions} = options; const {input, output, changelogs: changelogsSetting, included} = ArgvService.getConfig(); const resolvedInputPath = resolve(input, inputPath); const vars = getVarsPerFile(inputPath); - const content = await enrichWithFrontMatter({ + const content = await enrichWithFrontMatter(run, { fileContent: readFileSync(resolvedInputPath, 'utf8'), metadataOptions, resolvedFrontMatterVars: { diff --git a/src/services/contributors.ts b/src/services/contributors.ts index 63bce89e..3f3758f8 100644 --- a/src/services/contributors.ts +++ b/src/services/contributors.ts @@ -1,11 +1,11 @@ import {readFile} from 'fs/promises'; -import {dirname, join} from 'path'; +import {dirname, join} from 'node:path'; import {REGEXP_INCLUDE_CONTENTS, REGEXP_INCLUDE_FILE_PATH} from '../constants'; import {Contributor, Contributors} from '../models'; import {FileContributors, VCSConnector} from '../vcs-connector/connector-models'; export interface ContributorsServiceFileData { - resolvedFilePath: string; + resolvedFilePath: AbsolutePath; inputFolderPathLength: number; fileContent: string; } @@ -50,10 +50,7 @@ async function getContributorsForNestedFiles( } const includesContributors: Contributors[] = []; - const relativeIncludeFilePaths: Set = getRelativeIncludeFilePaths( - fileData, - includeContents, - ); + const relativeIncludeFilePaths = getRelativeIncludeFilePaths(fileData, includeContents); for (const relativeIncludeFilePath of relativeIncludeFilePaths.values()) { const relativeFilePath = relativeIncludeFilePath.substring(inputFolderPathLength); @@ -92,8 +89,8 @@ async function getContributorsForNestedFiles( function getRelativeIncludeFilePaths( {resolvedFilePath: tmpInputFilePath}: ContributorsServiceFileData, includeContents: string[], -): Set { - const relativeIncludeFilePaths: Set = new Set(); +): Set { + const relativeIncludeFilePaths: Set = new Set(); includeContents.forEach((includeContent: string) => { const relativeIncludeFilePath = includeContent.match(REGEXP_INCLUDE_FILE_PATH); @@ -121,13 +118,12 @@ export async function getFileIncludes(fileData: ContributorsServiceFileData) { if (!includeContents || includeContents.length === 0) { return []; } - const relativeIncludeFilePaths: Set = getRelativeIncludeFilePaths( - fileData, - includeContents, - ); + const relativeIncludeFilePaths = getRelativeIncludeFilePaths(fileData, includeContents); for (const relativeIncludeFilePath of relativeIncludeFilePaths.values()) { const relativeFilePath = relativeIncludeFilePath.substring(inputFolderPathLength + 1); - if (results.has(relativeFilePath)) continue; + if (results.has(relativeFilePath)) { + continue; + } results.add(relativeFilePath); let contentIncludeFile: string; diff --git a/src/services/includers/batteries/common.ts b/src/services/includers/batteries/common.ts deleted file mode 100644 index 447a1a08..00000000 --- a/src/services/includers/batteries/common.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -const { - promises: {readdir}, -} = require('fs'); - -async function getDirs(path: string) { - const isDir = (i: any) => i.isDirectory(); - - return readdir(path, {withFileTypes: true}).then((list: any) => list.filter(isDir)); -} - -async function getFiles(path: string) { - const isFile = (i: any) => i.isFile(); - - return readdir(path, {withFileTypes: true}).then((list: any) => list.filter(isFile)); -} - -const complement = (fn: Function) => (x: any) => !fn(x); - -const isMdExtension = (str: string): boolean => /.md$/gmu.test(str); - -const isHidden = (str: string) => /^\./gmu.test(str); - -const allPass = (predicates: Function[]) => (arg: any) => - predicates.map((fn) => fn(arg)).reduce((p, c) => p && c, true); - -const compose = (fn1: (a: R) => R, ...fns: Array<(a: R) => R>) => - fns.reduce((prevFn, nextFn) => (value) => prevFn(nextFn(value)), fn1); - -const prop = (string: string) => (object: Object) => object[string as keyof typeof object]; - -function concatNewLine(prefix: string, suffix: string) { - return prefix.trim().length ? `${prefix}
${suffix}` : suffix; -} - -export { - complement, - isMdExtension, - isHidden, - allPass, - compose, - prop, - getDirs, - getFiles, - concatNewLine, -}; diff --git a/src/services/includers/batteries/generic.ts b/src/services/includers/batteries/generic.ts deleted file mode 100644 index e6f12226..00000000 --- a/src/services/includers/batteries/generic.ts +++ /dev/null @@ -1,150 +0,0 @@ -import {mkdir, readFile, writeFile} from 'fs/promises'; -import {dirname, join, parse} from 'path'; - -import {updateWith} from 'lodash'; -import {dump} from 'js-yaml'; - -import {getRealPath} from '@diplodoc/transform/lib/utilsFS'; - -import {glob} from 'glob'; - -import {IncluderFunctionParams} from '../../../models'; - -class GenericIncluderError extends Error { - path: string; - - constructor(message: string, path: string) { - super(message); - - this.name = 'GenericIncluderError'; - this.path = path; - } -} - -const name = 'generic'; - -const MD_GLOB = '**/*.md'; - -type Params = { - input: string; - leadingPage: { - name?: string; - }; -}; - -async function includerFunction(params: IncluderFunctionParams) { - const { - readBasePath, - writeBasePath, - tocPath, - item, - passedParams: {input, leadingPage}, - index, - } = params; - - if (!input?.length || !item.include?.path) { - throw new GenericIncluderError('provide includer with input parameter', tocPath); - } - - try { - const leadingPageName = leadingPage?.name ?? 'Overview'; - - const tocDirPath = dirname(tocPath); - - const contentPath = - index === 0 - ? join(writeBasePath, tocDirPath, input) - : join(readBasePath, tocDirPath, input); - - const found = await glob(MD_GLOB, { - cwd: contentPath, - nocase: true, - }); - - const writePath = getRealPath(join(writeBasePath, tocDirPath, item.include.path)); - - if (!writePath.startsWith(writeBasePath)) { - throw new GenericIncluderError( - `Expected the include path to be located inside project root, got: ${writePath}`, - writePath, - ); - } - - await mkdir(writePath, {recursive: true}); - - for (const filePath of found) { - const file = await readFile(join(contentPath, filePath)); - - await mkdir(dirname(join(writePath, filePath)), {recursive: true}); - await writeFile(join(writePath, filePath), file); - } - - const graph = createGraphFromPaths(found); - - const toc = createToc(leadingPageName, item.include.path)(graph, []); - - await writeFile(join(writePath, 'toc.yaml'), dump(toc)); - } catch (err) { - throw new GenericIncluderError(err.toString(), tocPath); - } -} - -function createGraphFromPaths(paths: string[]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const graph: Record = {}; - - for (const path of paths) { - const chunks = path.split('/').filter(Boolean); - if (chunks.length < 2) { - if (chunks.length === 1) { - graph.files = graph.files ? graph.files.concat(chunks[0]) : chunks; - } - - continue; - } - - const file = chunks.pop(); - - updateWith( - graph, - chunks, - (old) => { - return old ? {files: [...old.files, file]} : {files: [file]}; - }, - Object, - ); - } - - return graph; -} - -function createToc(leadingPageName: string, tocName: string) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function createTocRec( - graph: Record, - cursor: string[], - ): Record { - const handler = (file: string) => ({ - name: parse(file).name === 'index' ? leadingPageName : file, - href: join(...cursor, file), - }); - - const recurse = (key: string) => createTocRec(graph[key], [...cursor, key]); - - const current = { - name: cursor[cursor.length - 1] ?? tocName, - items: [ - ...(graph.files ?? []).map(handler), - ...Object.keys(graph) - .filter((key) => key !== 'files') - .map(recurse), - ], - }; - - return current; - }; -} - -export {name, includerFunction}; - -export default {name, includerFunction}; diff --git a/src/services/includers/batteries/index.ts b/src/services/includers/batteries/index.ts deleted file mode 100644 index 0170b0a3..00000000 --- a/src/services/includers/batteries/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * as generic from './generic'; -export * as sourcedocs from './sourcedocs'; -export * as unarchive from './unarchive'; diff --git a/src/services/includers/batteries/sourcedocs.ts b/src/services/includers/batteries/sourcedocs.ts deleted file mode 100644 index 388399bc..00000000 --- a/src/services/includers/batteries/sourcedocs.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {logger} from '../../../utils/logger'; - -import generic from './generic'; - -import {IncluderFunctionParams} from '../../../models'; - -const name = 'sourcedocs'; - -const usage = `include: - path: - includers: - - name: generic - input: - leadingPage: - name: -`; - -type Params = { - input: string; - leadingPage: { - name?: string; - }; -}; - -async function includerFunction(params: IncluderFunctionParams) { - logger.warn( - params.tocPath, - `sourcedocs inlcuder is getting depricated in favor of generic includer\n${usage}`, - ); - - await generic.includerFunction(params); -} - -export {name, includerFunction}; - -export default {name, includerFunction}; diff --git a/src/services/includers/batteries/unarchive.ts b/src/services/includers/batteries/unarchive.ts deleted file mode 100644 index a743da0a..00000000 --- a/src/services/includers/batteries/unarchive.ts +++ /dev/null @@ -1,110 +0,0 @@ -import {createReadStream, createWriteStream, mkdirSync} from 'fs'; -import {dirname, join} from 'path'; -import {Headers, extract} from 'tar-stream'; - -import type {PassThrough} from 'stream'; - -import {getRealPath} from '@diplodoc/transform/lib/utilsFS'; - -import {IncluderFunctionParams} from '../../../models'; - -const name = 'unarchive'; - -class UnarchiveIncluderError extends Error { - path: string; - - constructor(message: string, path: string) { - super(message); - - this.name = 'UnarchiveIncluderError'; - this.path = path; - } -} - -function pipeline(readPath: string, writeBasePath: string): Promise { - return new Promise((res, rej) => { - const reader = createReadStream(readPath); - - reader.on('error', (err: Error) => { - rej(err); - }); - - const extractor = extract(); - - extractor.on('error', (err: Error) => { - rej(err); - }); - - mkdirSync(writeBasePath, {recursive: true}); - - extractor.on('entry', (header: Headers, stream: PassThrough, next: Function) => { - const {type, name} = header; - - const writePath = join(writeBasePath, name); - - const writeDirPath = type === 'directory' ? writePath : dirname(writePath); - - mkdirSync(writeDirPath, {recursive: true}); - - if (type !== 'directory') { - const writer = createWriteStream(writePath, {flags: 'w'}); - - writer.on('error', (err) => { - rej(err); - }); - - stream.pipe(writer); - } - - stream.on('end', () => { - next(); - }); - - stream.resume(); - }); - - reader.pipe(extractor).on('finish', () => { - res(); - }); - }); -} - -type Params = { - input: string; - output: string; -}; - -async function includerFunction(params: IncluderFunctionParams) { - const { - readBasePath, - writeBasePath, - tocPath, - passedParams: {input, output}, - index, - } = params; - - if (!input?.length || !output?.length) { - throw new UnarchiveIncluderError('provide includer with input parameter', tocPath); - } - - const contentPath = index === 0 ? join(writeBasePath, input) : join(readBasePath, input); - - const writePath = getRealPath(join(writeBasePath, output)); - - if (!writePath.startsWith(writeBasePath)) { - throw new UnarchiveIncluderError( - `Expected the output parameter to be located inside project root, got: ${output}`, - output, - ); - } - - try { - await pipeline(contentPath, writePath); - } catch (err) { - throw new UnarchiveIncluderError(err.toString(), tocPath); - } -} - -export {name, includerFunction}; - -export default {name, includerFunction}; diff --git a/src/services/includers/index.ts b/src/services/includers/index.ts deleted file mode 100644 index 402c3f22..00000000 --- a/src/services/includers/index.ts +++ /dev/null @@ -1,159 +0,0 @@ -import {join} from 'path'; - -import {isObject} from 'lodash'; - -import {ArgvService} from '../index'; -import {IncludeMode} from '../../constants'; -import {generic, sourcedocs, unarchive} from './batteries'; - -import type { - Includer, - YfmPreset, - YfmToc, - YfmTocInclude, - YfmTocIncluder, - YfmTocIncluders, -} from '../../models'; - -const includersUsage = `include: - path: - includers: - - name: - : - - name: - : -`; - -type IncludersMap = Record; - -let includersMap!: IncludersMap; - -class IncludersError extends Error { - path: string; - - constructor(message: string, path: string) { - super(message); - - this.name = 'IncludersError'; - this.path = path; - } -} - -function init(custom: Includer[] = []) { - if (includersMap) { - return; - } - - includersMap = {generic, sourcedocs, unarchive}; - - for (const includer of custom) { - includersMap[includer.name] = includer; - } -} - -async function applyIncluders(path: string, item: YfmToc, vars: YfmPreset) { - if (!item.include?.includers) { - return; - } - - if (!includeValid(item.include)) { - throw new IncludersError("include doesn't comply with includers standard", path); - } - - // normalize include mode (includers support link mode only) - item.include.mode = IncludeMode.LINK; - - const {status, message} = includersValid(item.include.includers); - if (!status) { - throw new IncludersError(message ?? '', path); - } - - let index = 0; - for (const {name, ...rest} of item.include.includers) { - const includer = getIncluder(name); - const passedParams = {...rest}; - - await applyIncluder({path, item, includer, passedParams, index, vars}); - } - - // contract to be fullfilled by the includer: - // provide builder generated toc.yaml - item.include.path = join(item.include.path, 'toc.yaml'); - index++; -} - -function includeValid(include: YfmTocInclude) { - return (include.mode === IncludeMode.LINK || !include.mode) && include.path?.length; -} - -function includersValid(includers: YfmTocIncluders) { - for (const includer of includers) { - const {status, message} = includerValid(includer); - - if (!status) { - return {status, message}; - } - } - - return {status: true}; -} - -function includerValid(includer: YfmTocIncluder) { - if (isObject(includer)) { - if (typeof includer.name !== 'string') { - return { - status: false, - message: 'use string in the includer.name to specify includers name', - }; - } - - if (includerExists(includer)) { - return {status: true}; - } - - return {status: false, message: `includer ${includer.name} not implemented`}; - } - - return { - status: false, - message: `use appropriate includers format:\n${includersUsage}`, - }; -} - -function getIncluder(includerName: string) { - return includersMap[includerName]; -} - -function includerExists(includer: YfmTocIncluder) { - return includersMap[includer.name as keyof typeof includersMap]; -} - -export type ApplyIncluderParams = { - path: string; - item: YfmToc; - includer: Includer; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - passedParams: Record; - index: number; - vars: YfmPreset; -}; - -async function applyIncluder(args: ApplyIncluderParams) { - const {rootInput: readBasePath, input: writeBasePath} = ArgvService.getConfig(); - - const {path, item, includer, passedParams, index, vars} = args; - - const params = { - tocPath: path, - passedParams, - index, - item, - readBasePath, - writeBasePath, - vars, - }; - - return await includer.includerFunction(params); -} - -export {init, applyIncluders, IncludersError}; diff --git a/src/services/index.ts b/src/services/index.ts index 1a4e2fdf..46898249 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -4,4 +4,3 @@ export {default as ArgvService} from './argv'; export {default as LeadingService} from './leading'; export {default as SearchService} from './search'; export * as PluginService from './plugins'; -export * as Includers from './includers'; diff --git a/src/services/metadata/enrich.ts b/src/services/metadata/enrich.ts index 30b5c94d..8d001873 100644 --- a/src/services/metadata/enrich.ts +++ b/src/services/metadata/enrich.ts @@ -1,4 +1,5 @@ import type {FrontMatter} from '@diplodoc/transform/lib/frontmatter'; +import type {Run} from '~/commands/build'; import {MetaDataOptions, VarsMetadata} from '../../models'; import {mergeFrontMatter} from './mergeMetadata'; @@ -24,18 +25,17 @@ const resolveVCSPath = (frontMatter: FrontMatter, relativeInputPath: string) => : relativeInputPath; }; -export const enrichWithFrontMatter = async ({ - fileContent, - metadataOptions, - resolvedFrontMatterVars, -}: EnrichWithFrontMatterOptions) => { +export const enrichWithFrontMatter = async ( + run: Run, + {fileContent, metadataOptions, resolvedFrontMatterVars}: EnrichWithFrontMatterOptions, +) => { const {systemVars, metadataVars} = resolvedFrontMatterVars; const {resources, addSystemMeta, shouldAlwaysAddVCSPath, pathData} = metadataOptions; const [frontMatter, strippedContent] = extractFrontMatter(fileContent, pathData.pathToFile); const vcsFrontMatter = metadataOptions.isContributorsEnabled - ? await resolveVCSFrontMatter(frontMatter, metadataOptions, fileContent) + ? await resolveVCSFrontMatter(run, frontMatter, metadataOptions, fileContent) : undefined; const mergedFrontMatter = mergeFrontMatter({ diff --git a/src/services/metadata/vcsMetadata.ts b/src/services/metadata/vcsMetadata.ts index 044f9139..4321c712 100644 --- a/src/services/metadata/vcsMetadata.ts +++ b/src/services/metadata/vcsMetadata.ts @@ -1,6 +1,7 @@ -import {TocService} from '..'; import type {FrontMatter} from '@diplodoc/transform/lib/frontmatter'; +import type {Run} from '~/commands/build'; +import {join, relative} from 'node:path'; import {Contributor, MetaDataOptions, Metadata, PathData} from '../../models'; import {VCSConnector} from '../../vcs-connector/connector-models'; import { @@ -10,6 +11,7 @@ import { } from '../authors'; import {ContributorsServiceFileData, getFileContributors, getFileIncludes} from '../contributors'; import {isObject} from '../utils'; +import {normalizePath} from '../../utils'; const getFileDataForContributorsService = ( pathData: PathData, @@ -22,7 +24,11 @@ const getFileDataForContributorsService = ( }; }; -const getModifiedTimeISOString = async (options: MetaDataOptions, fileContent: string) => { +const getModifiedTimeISOString = async ( + run: Run, + options: MetaDataOptions, + fileContent: string, +) => { const {isContributorsEnabled, vcsConnector, pathData} = options; const {pathToFile: relativeFilePath} = pathData; @@ -31,18 +37,16 @@ const getModifiedTimeISOString = async (options: MetaDataOptions, fileContent: s return undefined; } - const includedFiles = await getFileIncludes( - getFileDataForContributorsService(pathData, fileContent), - ); - includedFiles.push(relativeFilePath); - - const tocCopyFileMap = TocService.getCopyFileMap(); + const includedFiles = [relativeFilePath] + .concat(await getFileIncludes(getFileDataForContributorsService(pathData, fileContent))) + .map((path) => join(run.input, path)); - const mtimeList = includedFiles - .map((path) => { - const mappedPath = tocCopyFileMap.get(path) || path; - return vcsConnector.getModifiedTimeByPath(mappedPath); - }) + const mappedIncludedFiles = await Promise.all( + includedFiles.map((path) => run.realpath(path, false)), + ); + const mtimeList = mappedIncludedFiles + .map((path) => normalizePath(relative(run.input, path))) + .map((path) => vcsConnector.getModifiedTimeByPath(path)) .filter((v) => typeof v === 'number') as number[]; if (mtimeList.length) { @@ -104,6 +108,7 @@ export const getVCSMetadata = async ( }; export const resolveVCSFrontMatter = async ( + run: Run, existingMetadata: FrontMatter, options: MetaDataOptions, fileContent: string, @@ -121,7 +126,7 @@ export const resolveVCSFrontMatter = async ( const [author, contributors, updatedAt] = await Promise.all([ getAuthor(), getContributorsMetadata(options, fileContent), - getModifiedTimeISOString(options, fileContent), + getModifiedTimeISOString(run, options, fileContent), ]); const authorToSpread = author === null ? undefined : {author}; diff --git a/src/services/tocs.ts b/src/services/tocs.ts index 23cd2807..e87e962d 100644 --- a/src/services/tocs.ts +++ b/src/services/tocs.ts @@ -1,468 +1,41 @@ -import {dirname, extname, join, normalize, parse, relative, resolve, sep} from 'path'; -import {existsSync, readFileSync, writeFileSync} from 'fs'; -import {dump, load} from 'js-yaml'; -import shell from 'shelljs'; -import walkSync from 'walk-sync'; -import {liquidSnippet} from '@diplodoc/transform/lib/liquid'; -import log from '@diplodoc/transform/lib/log'; -import {bold} from 'chalk'; +import type {TocService} from '../commands/build/core/toc'; +import type {YfmToc} from '../models'; -import {ArgvService, PresetService} from './index'; -import {YfmToc} from '../models'; -import {IncludeMode, Stage} from '../constants'; -import {isExternalHref, logger} from '../utils'; -import {filterFiles, firstFilterItem, firstFilterTextItems, liquidField} from './utils'; -import {IncludersError, applyIncluders} from './includers'; -import {addSourcePath} from '../commands/build/core/meta'; +import {dirname} from 'node:path'; +import {normalizePath} from '../utils'; export interface TocServiceData { - storage: Map; - tocs: Map; navigationPaths: string[]; - includedTocPaths: Set; } -const storage: TocServiceData['storage'] = new Map(); -const paths: Map = new Map(); -const tocs: TocServiceData['tocs'] = new Map(); -let navigationPaths: TocServiceData['navigationPaths'] = []; -const includedTocPaths: TocServiceData['includedTocPaths'] = new Set(); -const tocFileCopyMap = new Map(); +let navigationPaths: TocServiceData['navigationPaths']; -async function init(tocFilePaths: string[]) { - for (const path of tocFilePaths) { - logger.proc(path); - - await add(path); - await addNavigation(path); - } -} - -async function add(path: string) { - const { - input: inputFolderPath, - output: outputFolderPath, - outputFormat, - ignoreStage, - vars, - } = ArgvService.getConfig(); - - const pathToDir = dirname(path); - const content = readFileSync(resolve(inputFolderPath, path), 'utf8'); - const parsedToc = load(content) as YfmToc; - - // Should ignore toc with specified stage. - if (parsedToc.stage === ignoreStage) { - return; - } - - const combinedVars = { - ...PresetService.get(pathToDir), - ...vars, - }; - - if (parsedToc.title) { - parsedToc.title = firstFilterTextItems(parsedToc.title, combinedVars, { - resolveConditions: true, - }); - } - - if (typeof parsedToc.title === 'string') { - parsedToc.title = liquidField(parsedToc.title, combinedVars, path); - } - - if (typeof parsedToc.navigation === 'string') { - parsedToc.navigation = liquidField(parsedToc.navigation, combinedVars, path); - } - - if (parsedToc.label) { - parsedToc.label = firstFilterItem(parsedToc.label, combinedVars, { - resolveConditions: true, - }); - } - - parsedToc.items = await processTocItems( - path, - parsedToc.items, - join(inputFolderPath, pathToDir), - resolve(inputFolderPath), - combinedVars, - ); - - /* Store parsed toc for .md output format */ - storage.set(path, parsedToc); - - /* save toc in distinct set, storage includes .md files too */ - tocs.set(path, parsedToc); - - if (outputFormat === 'md') { - /* Should copy resolved and filtered toc to output folder */ - const outputPath = resolve(outputFolderPath, path); - const outputToc = dump(parsedToc); - shell.mkdir('-p', dirname(outputPath)); - writeFileSync(outputPath, outputToc); - } -} - -// To collect root toc.yaml we need to move from root into deep -async function addNavigation(path: string) { - const parsedToc = getForPath(path)[1]; - - if (!parsedToc) { - return; - } - - const pathToDir = dirname(path); - prepareNavigationPaths(parsedToc, pathToDir); -} - -async function processTocItems( - path: string, - items: YfmToc[], - tocDir: string, - sourcesDir: string, - vars: Record, -) { - const {resolveConditions, removeHiddenTocItems} = ArgvService.getConfig(); - - let preparedItems = items; - - /* Should remove all links with false expressions */ - if (resolveConditions || removeHiddenTocItems) { - try { - preparedItems = filterFiles(items, 'items', vars, { - resolveConditions, - removeHiddenTocItems, - }); - } catch (error) { - log.error(`Error while filtering toc file: ${path}. Error message: ${error}`); - } - } - - /* Should resolve all includes */ - return _replaceIncludes(path, preparedItems, tocDir, sourcesDir, vars); +let toc: TocService; +async function init(service: TocService) { + toc = service; + setNavigationPaths(toc.entries); } function getForPath(path: string): [string | null, YfmToc | null] { - let tocPath = paths.get(path) || null; - let tocData = storage.get(path) || null; - - // TODO: normalize paths on add - if (!tocData && path.endsWith('.md')) { - path = path.replace('.md', ''); - tocPath = paths.get(path) || null; - tocData = storage.get(path) || null; - } - - if (!tocData && path.endsWith('index.yaml')) { - path = path.replace('index.yaml', ''); - tocPath = paths.get(path) || null; - tocData = storage.get(path) || null; - } - - return [tocPath, tocData]; + return toc.for(normalizePath(path)) as unknown as [string, YfmToc]; } function getNavigationPaths(): string[] { - return [...navigationPaths]; -} - -function getIncludedTocPaths(): string[] { - return [...includedTocPaths]; -} - -function prepareNavigationPaths(parsedToc: YfmToc, dirPath: string) { - function processItems(items: YfmToc[], pathToDir: string) { - items.forEach((item) => { - if (!parsedToc.singlePage && item.items) { - const preparedSubItems = item.items.map((yfmToc: YfmToc, index: number) => { - // Generate personal id for each navigation item - yfmToc.id = `${yfmToc.name}-${index}-${Math.random()}`; - return yfmToc; - }); - processItems(preparedSubItems, pathToDir); - } - - if (item.href && !isExternalHref(item.href)) { - const href = join(pathToDir, item.href); - storage.set(href, parsedToc); - paths.set(href, dirPath); - - const navigationPath = _normalizeHref(href); - navigationPaths.push(navigationPath); - } - }); - } - - processItems([parsedToc], dirPath); -} - -/** - * Should normalize hrefs. MD and YAML files will be ignored. - * @param href - * @return {string} - * @example instance-groups/create-with-coi/ -> instance-groups/create-with-coi/index.yaml - * @example instance-groups/create-with-coi -> instance-groups/create-with-coi.md - * @private - */ -function _normalizeHref(href: string): string { - const preparedHref = normalize(href); - - if (preparedHref.endsWith('.md') || preparedHref.endsWith('.yaml')) { - return preparedHref; - } - - if (preparedHref.endsWith(sep)) { - return `${preparedHref}index.yaml`; - } - - return `${preparedHref}.md`; -} - -/** - * Copies all files of include toc to original dir. - * @param tocPath - * @param destDir - * @return - * @private - */ -function _copyTocDir(tocPath: string, destDir: string) { - const {input: inputFolderPath} = ArgvService.getConfig(); - - const {dir: tocDir} = parse(tocPath); - const files: string[] = walkSync(tocDir, { - globs: ['**/*.*'], - ignore: ['**/toc.yaml'], - directories: false, - }); - - files.forEach((relPath) => { - const from = resolve(tocDir, relPath); - const to = resolve(destDir, relPath); - const fileExtension = extname(relPath); - const isMdFile = fileExtension === '.md'; - - shell.mkdir('-p', parse(to).dir); - - if (isMdFile) { - const fileContent = readFileSync(from, 'utf8'); - const sourcePath = relative(inputFolderPath, from); - const updatedFileContent = addSourcePath(fileContent, sourcePath); - - writeFileSync(to, updatedFileContent); - } else { - shell.cp(from, to); - } - - const relFrom = relative(inputFolderPath, from); - const relTo = relative(inputFolderPath, to); - tocFileCopyMap.set(relTo, relFrom); - }); -} - -/** - * Make hrefs relative to the main toc in the included toc. - * @param items - * @param includeTocDir - * @param tocDir - * @return - * @private - */ -function _replaceIncludesHrefs(items: YfmToc[], includeTocDir: string, tocDir: string): YfmToc[] { - return items.reduce((acc, tocItem) => { - if (tocItem.href) { - tocItem.href = relative(tocDir, resolve(includeTocDir, tocItem.href)); - } - - if (tocItem.items) { - tocItem.items = _replaceIncludesHrefs(tocItem.items, includeTocDir, tocDir); - } - - if (tocItem.include) { - const {path} = tocItem.include; - tocItem.include.path = relative(tocDir, resolve(includeTocDir, path)); - } - - return acc.concat(tocItem); - }, [] as YfmToc[]); -} - -/** - * Liquid substitutions in toc file. - * @param input - * @param vars - * @param path - * @return {string} - * @private - */ -function _liquidSubstitutions(input: string, vars: Record, path: string) { - const {outputFormat, applyPresets} = ArgvService.getConfig(); - if (outputFormat === 'md' && !applyPresets) { - return input; - } - - return liquidSnippet(input, vars, path, { - conditions: false, - substitutions: true, - }); -} - -function addIncludeTocPath(includeTocPath: string) { - includedTocPaths.add(includeTocPath); -} - -/** - * Replaces include fields in toc file by resolved toc. - * @param path - * @param items - * @param tocDir - * @param sourcesDir - * @param vars - * @return - * @private - */ -async function _replaceIncludes( - path: string, - items: YfmToc[], - tocDir: string, - sourcesDir: string, - vars: Record, -): Promise { - const result: YfmToc[] = []; - - for (const item of items) { - let includedInlineItems: YfmToc[] | null = null; - - if (item.name) { - const tocPath = join(tocDir, 'toc.yaml'); - - item.name = _liquidSubstitutions(item.name, vars, tocPath); - } - - try { - await applyIncluders(path, item, vars); - } catch (err) { - if (err instanceof Error || err instanceof IncludersError) { - const message = err.toString(); - - const file = err instanceof IncludersError ? err.path : path; - - logger.error(file, message); - } - } - - if (item.include) { - const {mode = IncludeMode.ROOT_MERGE} = item.include; - const includeTocPath = - mode === IncludeMode.ROOT_MERGE - ? resolve(sourcesDir, item.include.path) - : resolve(tocDir, item.include.path); - const includeTocDir = dirname(includeTocPath); - - try { - const includeToc = load(readFileSync(includeTocPath, 'utf8')) as YfmToc; - - // Should ignore included toc with tech-preview stage. - if (includeToc.stage === Stage.TECH_PREVIEW) { - continue; - } - - if (mode === IncludeMode.MERGE || mode === IncludeMode.ROOT_MERGE) { - _copyTocDir(includeTocPath, tocDir); - } - - /* Save the path to exclude toc from the output directory in the next step */ - addIncludeTocPath(includeTocPath); - - let includedTocItems = (item.items || []).concat(includeToc.items); - - /* Resolve nested toc inclusions */ - const baseTocDir = mode === IncludeMode.LINK ? includeTocDir : tocDir; - includedTocItems = await processTocItems( - path, - includedTocItems, - baseTocDir, - sourcesDir, - vars, - ); - - /* Make hrefs relative to the main toc */ - if (mode === IncludeMode.LINK) { - includedTocItems = _replaceIncludesHrefs( - includedTocItems, - includeTocDir, - tocDir, - ); - } - - if (item.name) { - item.items = includedTocItems; - } else { - includedInlineItems = includedTocItems; - } - } catch (err) { - const message = `Error while including toc: ${bold(includeTocPath)} to ${bold( - join(tocDir, 'toc.yaml'), - )}`; - - log.error(message); - - continue; - } finally { - delete item.include; - } - } else if (item.items) { - item.items = await processTocItems(path, item.items, tocDir, sourcesDir, vars); - } - - if (includedInlineItems) { - result.push(...includedInlineItems); - } else { - result.push(item); - } - } - - return result; + return navigationPaths || [...toc.entries]; } function getTocDir(pagePath: string): string { - const {input: inputFolderPath} = ArgvService.getConfig(); - - const tocDir = dirname(pagePath); - const tocPath = resolve(tocDir, 'toc.yaml'); - - if (!tocDir.includes(inputFolderPath)) { - throw new Error('Error while finding toc dir'); - } - - if (existsSync(tocPath)) { - return tocDir; - } - - return getTocDir(tocDir); + return dirname(toc.for(normalizePath(pagePath))[0]); } function setNavigationPaths(paths: TocServiceData['navigationPaths']) { navigationPaths = paths; } -function getCopyFileMap() { - return tocFileCopyMap; -} - -function getAllTocs() { - return [...tocs.entries()]; -} - export default { init, - add, - process, getForPath, getNavigationPaths, getTocDir, - getIncludedTocPaths, setNavigationPaths, - getCopyFileMap, - getAllTocs, }; diff --git a/src/services/utils.ts b/src/services/utils.ts index 16c5d30d..7c3397c2 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -93,26 +93,6 @@ export function firstFilterTextItems( return filteredItems[0] || ''; } -export function firstFilterItem( - itemOrItems: T | T[], - vars: Record, - options?: FilterFilesOptions, -) { - const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - - const filteredItems = items.reduce((result: T[], item) => { - const useItem = shouldProcessItem(item, vars, options); - - if (useItem) { - result.push(item); - } - - return result; - }, []); - - return filteredItems[0]; -} - function shouldProcessItem( item: T, vars: Record, diff --git a/src/steps/index.ts b/src/steps/index.ts index dc7c243b..4e7442fb 100644 --- a/src/steps/index.ts +++ b/src/steps/index.ts @@ -3,5 +3,4 @@ export * from './processExcludedFiles'; export * from './processLogs'; export * from './processPages'; export * from './processLinter'; -export * from './processServiceFiles'; export * from './processChangelogs'; diff --git a/src/steps/processExcludedFiles.ts b/src/steps/processExcludedFiles.ts index 20fb99f2..e855f10f 100644 --- a/src/steps/processExcludedFiles.ts +++ b/src/steps/processExcludedFiles.ts @@ -1,4 +1,4 @@ -import {relative, resolve} from 'path'; +import {resolve} from 'path'; import walkSync from 'walk-sync'; import shell from 'shelljs'; @@ -10,7 +10,7 @@ import {convertBackSlashToSlash} from '../utils'; * @return {void} */ export function processExcludedFiles() { - const {input: inputFolderPath, output: outputFolderPath, ignore} = ArgvService.getConfig(); + const {input: inputFolderPath, ignore} = ArgvService.getConfig(); const allContentFiles: string[] = walkSync(inputFolderPath, { directories: false, @@ -28,15 +28,4 @@ export function processExcludedFiles() { if (excludedFiles.length) { shell.rm('-f', excludedFiles); } - - const includedTocPaths = TocService.getIncludedTocPaths().map((filePath) => { - const relativeTocPath = relative(inputFolderPath, filePath); - const destTocPath = resolve(outputFolderPath, relativeTocPath); - - return convertBackSlashToSlash(destTocPath); - }); - - if (includedTocPaths.length) { - shell.rm('-rf', includedTocPaths); - } } diff --git a/src/steps/processMapFile.ts b/src/steps/processMapFile.ts index c3ccc1aa..c867233a 100644 --- a/src/steps/processMapFile.ts +++ b/src/steps/processMapFile.ts @@ -1,9 +1,8 @@ +import type {Run} from '~/commands/build'; + import {writeFileSync} from 'fs'; import {extname, join} from 'path'; -import {ArgvService, TocService} from '../services'; -import {convertBackSlashToSlash} from '../utils'; - type TocItem = { name: string; items?: TocItems; @@ -12,11 +11,9 @@ type TocItem = { type TocItems = TocItem[]; -export function prepareMapFile(): void { - const {output: outputFolderPath} = ArgvService.getConfig(); - - const navigationPathsWithoutExtensions = TocService.getNavigationPaths().map((path) => { - let preparedPath = convertBackSlashToSlash(path.replace(extname(path), '')); +export function prepareMapFile(run: Run): void { + const navigationPathsWithoutExtensions = run.toc.entries.map((path) => { + let preparedPath = path.replace(extname(path), ''); if (preparedPath.endsWith('/index')) { preparedPath = preparedPath.substring(0, preparedPath.length - 5); @@ -26,7 +23,7 @@ export function prepareMapFile(): void { }); const navigationPaths = {files: [...new Set(navigationPathsWithoutExtensions)].sort()}; const filesMapBuffer = Buffer.from(JSON.stringify(navigationPaths, null, '\t'), 'utf8'); - const mapFile = join(outputFolderPath, 'files.json'); + const mapFile = join(run.output, 'files.json'); writeFileSync(mapFile, filesMapBuffer); } diff --git a/src/steps/processPages.ts b/src/steps/processPages.ts index 9165b91b..c1a53836 100644 --- a/src/steps/processPages.ts +++ b/src/steps/processPages.ts @@ -1,13 +1,12 @@ import type {DocInnerProps} from '@diplodoc/client'; import type {Run} from '~/commands/build'; import {basename, dirname, extname, join, resolve} from 'node:path'; -import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs'; +import {existsSync, readFileSync, writeFileSync} from 'fs'; import log from '@diplodoc/transform/lib/log'; import {asyncify, mapLimit} from 'async'; import {bold} from 'chalk'; import {dump, load} from 'js-yaml'; import shell from 'shelljs'; -import dedent from 'ts-dedent'; import { Lang, @@ -28,7 +27,7 @@ import {resolveMd2HTML, resolveMd2Md} from '../resolvers'; import {ArgvService, LeadingService, PluginService, SearchService, TocService} from '../services'; import {generateStaticMarkup} from '~/pages/document'; import {generateStaticRedirect} from '~/pages/redirect'; -import {getDepth, joinSinglePageResults, transformToc, transformTocForSinglePage} from '../utils'; +import {getDepth, joinSinglePageResults} from '../utils'; import {getVCSConnector} from '../vcs-connector'; import {VCSConnector} from '../vcs-connector/connector-models'; @@ -37,29 +36,19 @@ const singlePagePaths: Record> = {}; // Processes files of documentation (like index.yaml, *.md) export async function processPages(run: Run): Promise { - const { - input: inputFolderPath, - output: outputFolderPath, - outputFormat, - singlePage, - resolveConditions, - } = ArgvService.getConfig(); - const vcsConnector = await getVCSConnector(); PluginService.setPlugins(); - const navigationPaths = TocService.getNavigationPaths(); - await mapLimit( - navigationPaths, + run.toc.entries, PAGE_PROCESS_CONCURRENCY, asyncify(async (pathToFile: string) => { const pathData = getPathData( pathToFile, - inputFolderPath, - outputFolderPath, - outputFormat, + run.input, + run.output, + run.config.outputFormat, run.bundlePath, ); @@ -68,24 +57,21 @@ export async function processPages(run: Run): Promise { const metaDataOptions = getMetaDataOptions(pathData, vcsConnector); await preparingPagesByOutputFormat( + run, pathData, metaDataOptions, - resolveConditions, - singlePage, + run.config.template.features.conditions, + run.config.singlePage, ); }), ); - if (singlePage) { - if (outputFormat === 'html') { - await saveTocData(transformTocForSinglePage, 'single-page-toc'); - } + if (run.config.singlePage) { await saveSinglePages(run); } - if (outputFormat === 'html') { - saveRedirectPage(outputFolderPath); - await saveTocData(transformToc, 'toc'); + if (run.config.outputFormat === 'html') { + saveRedirectPage(run); } } @@ -104,7 +90,7 @@ function getPathData( const outputFileName = `${fileBaseName}.${outputFormat}`; const outputPath = resolve(outputDir, outputFileName); const resolvedPathToFile = resolve(inputFolderPath, pathToFile); - const outputTocDir = TocService.getTocDir(resolvedPathToFile); + const outputTocDir = TocService.getTocDir(pathToFile); const pathData: PathData = { pathToFile, @@ -124,23 +110,6 @@ function getPathData( return pathData; } -async function saveTocData(transform: (toc: YfmToc, tocDir: string) => YfmToc, filename: string) { - const tocs = TocService.getAllTocs(); - const {output} = ArgvService.getConfig(); - - for (const [path, toc] of tocs) { - const outputPath = join(output, dirname(path), filename + '.js'); - mkdirSync(dirname(outputPath), {recursive: true}); - writeFileSync( - outputPath, - dedent` - window.__DATA__.data.toc = ${JSON.stringify(transform(toc, dirname(path)))}; - `, - 'utf8', - ); - } -} - async function saveSinglePages(run: Run) { try { await Promise.all( @@ -149,10 +118,7 @@ async function saveSinglePages(run: Run) { return; } - const relativeTocDir = tocDir - .replace(run.input, '') - .replace(/\\/g, '/') - .replace(/^\/?/, ''); + const relativeTocDir = tocDir.replace(/\\/g, '/').replace(/^\/?/, ''); const singlePageBody = joinSinglePageResults( singlePageResults[tocDir], relativeTocDir, @@ -179,8 +145,8 @@ async function saveSinglePages(run: Run) { }; // Save the full single page for viewing locally - const singlePageFn = join(tocDir, SINGLE_PAGE_FILENAME); - const singlePageDataFn = join(tocDir, SINGLE_PAGE_DATA_FILENAME); + const singlePageFn = join(run.output, tocDir, SINGLE_PAGE_FILENAME); + const singlePageDataFn = join(run.output, tocDir, SINGLE_PAGE_DATA_FILENAME); const singlePageContent = generateStaticMarkup( pageData, {path: join(relativeTocDir, 'single-page-toc'), content: toc}, @@ -196,17 +162,13 @@ async function saveSinglePages(run: Run) { } } -function saveRedirectPage(outputDir: string): void { - const {lang, langs} = ArgvService.getConfig(); - - const redirectLang = lang || langs?.[0] || Lang.RU; - const redirectLangRelativePath = `./${redirectLang}/index.html`; - - const redirectPagePath = join(outputDir, 'index.html'); - const redirectLangPath = join(outputDir, redirectLangRelativePath); +function saveRedirectPage(run: Run): void { + const redirectLangRelativePath = `./${run.config.lang}/index.html`; + const redirectPagePath = join(run.output, 'index.html'); + const redirectLangPath = join(run.output, redirectLangRelativePath); if (!existsSync(redirectPagePath) && existsSync(redirectLangPath)) { - const content = generateStaticRedirect(redirectLang, redirectLangRelativePath); + const content = generateStaticRedirect(run.config.lang, redirectLangRelativePath); writeFileSync(redirectPagePath, content); } } @@ -263,6 +225,7 @@ function getMetaDataOptions(pathData: PathData, vcsConnector?: VCSConnector): Me } async function preparingPagesByOutputFormat( + run: Run, path: PathData, metaDataOptions: MetaDataOptions, resolveConditions: boolean, @@ -303,10 +266,10 @@ async function preparingPagesByOutputFormat( switch (outputFormat) { case 'md': - await processingFileToMd(path, metaDataOptions); + await processingFileToMd(run, path, metaDataOptions); return; case 'html': { - const resolvedFileProps = await processingFileToHtml(path, metaDataOptions); + const resolvedFileProps = await processingFileToHtml(run, path, metaDataOptions); SearchService.add(pathToFile, resolvedFileProps); @@ -349,10 +312,14 @@ function copyFileWithoutChanges( shell.cp(from, to); } -async function processingFileToMd(path: PathData, metaDataOptions: MetaDataOptions): Promise { +async function processingFileToMd( + run: Run, + path: PathData, + metaDataOptions: MetaDataOptions, +): Promise { const {outputPath, pathToFile} = path; - await resolveMd2Md({ + await resolveMd2Md(run, { inputPath: pathToFile, outputPath, metadata: metaDataOptions, @@ -360,12 +327,13 @@ async function processingFileToMd(path: PathData, metaDataOptions: MetaDataOptio } async function processingFileToHtml( + run: Run, path: PathData, metaDataOptions: MetaDataOptions, ): Promise { const {outputBundlePath, filename, fileExtension, outputPath, pathToFile} = path; - return resolveMd2HTML({ + return resolveMd2HTML(run, { inputPath: pathToFile, outputBundlePath, fileExtension, diff --git a/src/steps/processServiceFiles.ts b/src/steps/processServiceFiles.ts deleted file mode 100644 index 9c1fafe1..00000000 --- a/src/steps/processServiceFiles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import walkSync from 'walk-sync'; -import log from '@diplodoc/transform/lib/log'; - -import {ArgvService, TocService} from '../services'; - -const getFilePathsByGlobals = (globs: string[]): string[] => { - const {input, ignore = []} = ArgvService.getConfig(); - - return walkSync(input, { - directories: false, - includeBasePath: false, - globs, - ignore, - }); -}; - -export async function preparingTocFiles(): Promise { - try { - const tocFilePaths = getFilePathsByGlobals(['**/toc.yaml']); - await TocService.init(tocFilePaths); - } catch (error) { - log.error(`Preparing toc.yaml files failed. Error: ${error}`); - throw error; - } -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 87876f31..84c191d5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,7 +3,6 @@ export * from './logger'; export * from './singlePage'; export * from './url'; export * from './path'; -export * from './toc'; export * from './presets'; export * from './file'; export * from './decorators'; diff --git a/src/utils/singlePage.ts b/src/utils/singlePage.ts index b9e83289..74f37ae4 100644 --- a/src/utils/singlePage.ts +++ b/src/utils/singlePage.ts @@ -4,6 +4,8 @@ import HTMLElement from 'node-html-parser/dist/nodes/html'; import {parse} from 'node-html-parser'; import {dirname, join} from 'path'; +import {getSinglePageUrl} from '../commands/build/features/singlepage/utils'; + interface PreprocessSinglePageOptions { path: string; tocDir: string; @@ -21,17 +23,6 @@ function toUrl(path: string) { return path.replace(/\\/g, '/').replace(/^\.\//, ''); } -function relativeTo(root: string, path: string) { - root = toUrl(root); - path = toUrl(path); - - if (root && path.startsWith(root + '/')) { - path = path.replace(root + '/', ''); - } - - return path; -} - function all(root: HTMLElement, selector: string): HTMLElement[] { return Array.from(root.querySelectorAll(selector)); } @@ -122,25 +113,6 @@ export function addMainTitle(root: HTMLElement, options: PreprocessSinglePageOpt } } -function getAnchorId(tocDir: string, path: string) { - const [pathname, hash] = path.split('#'); - const url = toUrl(dropExt(pathname)) + (hash ? '#' + hash : ''); - - // TODO: encodeURIComponent will be best option - return relativeTo(tocDir, url.replace(/\.\.\/|[/#]/g, '_')); -} - -export function getSinglePageUrl(tocDir: string, path: string) { - const prefix = toUrl(tocDir) || '.'; - const suffix = getAnchorId(tocDir, path); - - if (prefix === '.') { - return '#' + suffix; - } - - return prefix + '/single-page.html#' + suffix; -} - export function joinSinglePageResults( singlePageResults: SinglePageResult[], tocDir: string, diff --git a/src/utils/toc.ts b/src/utils/toc.ts deleted file mode 100644 index 9f903039..00000000 --- a/src/utils/toc.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type {YfmToc} from '~/models'; - -import {basename, dirname, extname, join} from 'node:path'; - -import {filterFiles} from '../services/utils'; -import {isExternalHref} from './url'; -import {getSinglePageUrl} from './singlePage'; - -function baseTransformToc(toc: YfmToc, transformItemHref: (href: string) => string): YfmToc { - const localToc: YfmToc = JSON.parse(JSON.stringify(toc)); - - if (localToc.items) { - localToc.items = filterFiles( - localToc.items, - 'items', - {}, - { - removeHiddenTocItems: true, - }, - ); - } - - const queue = [localToc]; - - while (queue.length) { - const item = queue.shift(); - - if (!item) { - continue; - } - - const {items, href} = item; - - if (items) { - queue.push(...items); - } - - if (href) { - item.href = transformItemHref(href); - } - } - - return localToc; -} - -export function transformToc(toc: YfmToc, tocDir: string) { - return baseTransformToc(toc, (href: string) => { - if (isExternalHref(href)) { - return href; - } - - if (href.endsWith('/')) { - href += 'index.yaml'; - } - - const fileExtension: string = extname(href); - const filename: string = basename(href, fileExtension) + '.html'; - - return join(tocDir, dirname(href), filename); - }); -} - -export function transformTocForSinglePage(toc: YfmToc, tocDir: string) { - return baseTransformToc(toc, (href: string) => { - if (isExternalHref(href)) { - return href; - } - - return getSinglePageUrl(tocDir, href); - }); -} diff --git a/src/utils/url.ts b/src/utils/url.ts index 71029bc6..88523b5e 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,3 +1,3 @@ export function isExternalHref(href: string) { - return href.startsWith('http') || href.startsWith('//'); + return /^(\w{1,10}:)?\/\//.test(href); } diff --git a/tests/e2e/__snapshots__/include-toc.test.ts.snap b/tests/e2e/__snapshots__/include-toc.test.ts.snap index 06154c5c..26d547bb 100644 --- a/tests/e2e/__snapshots__/include-toc.test.ts.snap +++ b/tests/e2e/__snapshots__/include-toc.test.ts.snap @@ -224,55 +224,10 @@ exports[`Include toc Toc is included inline, not as a new section: filelist 1`] "fileB.md", "fileC.md", "fileX.md", - "folder/fileA.md", - "folder/fileB.md", - "folder/fileC.md", - "folder/folder/fileC.md", "toc.yaml" ]" `; -exports[`Include toc Toc is included inline, not as a new section: folder/fileA.md 1`] = ` -"--- -title: File A -description: YFM description ---- -# File A - ---- - -Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. - ---- -" -`; - -exports[`Include toc Toc is included inline, not as a new section: folder/fileB.md 1`] = ` -"# File B - -Lorem Ipsum is simply dummy text of the printing and typesetting industry. - ---- - -Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. - ---- -" -`; - -exports[`Include toc Toc is included inline, not as a new section: folder/fileC.md 1`] = ` -"--- -sourcePath: folder/folder/fileC.md ---- -# File C -" -`; - -exports[`Include toc Toc is included inline, not as a new section: folder/folder/fileC.md 1`] = ` -"# File C -" -`; - exports[`Include toc Toc is included inline, not as a new section: toc.yaml 1`] = ` "items: - name: Name1 diff --git a/tests/e2e/__snapshots__/load-custom-resources.spec.ts.snap b/tests/e2e/__snapshots__/load-custom-resources.spec.ts.snap index 14efbcae..c00c1d15 100644 --- a/tests/e2e/__snapshots__/load-custom-resources.spec.ts.snap +++ b/tests/e2e/__snapshots__/load-custom-resources.spec.ts.snap @@ -332,9 +332,9 @@ exports[`Allow load custom resources md2html single page with custom resources: exports[`Allow load custom resources md2html single page with custom resources: single-page.json 1`] = `"{"data":{"leading":false,"html":"

Lorem

/n

Lorem

/n","headings":[],"meta":{"style":["_assets/style/test.css"],"script":["_assets/script/test1.js"]},"title":"Documentation"},"router":{"pathname":"single-page.html","depth":2},"lang":"ru","langs":["ru"]}"`; -exports[`Allow load custom resources md2html single page with custom resources: single-page-toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"#index","items":[{"name":"Documentation","href":"#page","id":"Documentation-RANDOM"},{"name":"Config","href":"#project_config","id":"Config-RANDOM"}]};"`; +exports[`Allow load custom resources md2html single page with custom resources: single-page-toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"#index","items":[{"name":"Documentation","href":"#page"},{"name":"Config","href":"#project_config"}]};"`; -exports[`Allow load custom resources md2html single page with custom resources: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html","id":"Documentation-RANDOM"},{"name":"Config","href":"project/config.html","id":"Config-RANDOM"}]};"`; +exports[`Allow load custom resources md2html single page with custom resources: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html"},{"name":"Config","href":"project/config.html"}]};"`; exports[`Allow load custom resources md2html with custom resources: .yfm 1`] = ` "resources: @@ -596,7 +596,7 @@ exports[`Allow load custom resources md2html with custom resources: project/conf `; -exports[`Allow load custom resources md2html with custom resources: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html","id":"Documentation-RANDOM"},{"name":"Config","href":"project/config.html","id":"Config-RANDOM"}]};"`; +exports[`Allow load custom resources md2html with custom resources: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html"},{"name":"Config","href":"project/config.html"}]};"`; exports[`Allow load custom resources md2md with custom resources: .yfm 1`] = ` "resources: diff --git a/tests/e2e/__snapshots__/metadata.spec.ts.snap b/tests/e2e/__snapshots__/metadata.spec.ts.snap index b7139ce1..701d06d4 100644 --- a/tests/e2e/__snapshots__/metadata.spec.ts.snap +++ b/tests/e2e/__snapshots__/metadata.spec.ts.snap @@ -248,7 +248,7 @@ exports[`Allow load custom resources md2html with metadata: project/config.html `; -exports[`Allow load custom resources md2html with metadata: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html","id":"Documentation-RANDOM"},{"name":"Config","href":"project/config.html","id":"Config-RANDOM"}]};"`; +exports[`Allow load custom resources md2html with metadata: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html"},{"name":"Config","href":"project/config.html"}]};"`; exports[`Allow load custom resources md2md with metadata: filelist 1`] = ` "[ diff --git a/tests/e2e/__snapshots__/rtl.spec.ts.snap b/tests/e2e/__snapshots__/rtl.spec.ts.snap index 815b870f..215fc9f3 100644 --- a/tests/e2e/__snapshots__/rtl.spec.ts.snap +++ b/tests/e2e/__snapshots__/rtl.spec.ts.snap @@ -163,7 +163,7 @@ exports[`Generate html document with correct lang and dir attributes. Load corre `; -exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with only one rtl lang: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html","id":"Documentation-RANDOM"}]};"`; +exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with only one rtl lang: toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"index.html","items":[{"name":"Documentation","href":"page.html"}]};"`; exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: .yfm 1`] = `"langs: ['ar', 'en']"`; @@ -279,7 +279,7 @@ exports[`Generate html document with correct lang and dir attributes. Load corre `; -exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: ar/toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"ar/index.html","items":[{"name":"Documentation","href":"ar/page.html","id":"Documentation-RANDOM"}]};"`; +exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: ar/toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"ar/index.html","items":[{"name":"Documentation","href":"ar/page.html"}]};"`; exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: en/index.html 1`] = ` @@ -402,7 +402,7 @@ exports[`Generate html document with correct lang and dir attributes. Load corre `; -exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: en/toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"en/index.html","items":[{"name":"Documentation","href":"en/page.html","id":"Documentation-RANDOM"}]};"`; +exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: en/toc.js 1`] = `"window.__DATA__.data.toc = {"title":"Documentation","href":"en/index.html","items":[{"name":"Documentation","href":"en/page.html"}]};"`; exports[`Generate html document with correct lang and dir attributes. Load correct bundles. documentation with rtl and ltr langs: filelist 1`] = ` "[ diff --git a/tests/integrations/services/metadataAuthors.test.ts b/tests/integrations/services/metadataAuthors.test.ts index dc1fbe1a..67be01ee 100644 --- a/tests/integrations/services/metadataAuthors.test.ts +++ b/tests/integrations/services/metadataAuthors.test.ts @@ -1,3 +1,5 @@ +import type {Run} from 'commands/build'; + import {extractFrontMatter} from '@diplodoc/transform/lib/frontmatter'; import {readFileSync} from 'fs'; import {MetaDataOptions} from 'models'; @@ -30,6 +32,11 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { getModifiedTimeByPath: () => undefined, }; + const run = { + input: '', + realpath: (path: string) => path, + } as unknown as Run; + describe('should return file content with updated author in metadata', () => { let metadataOptions: MetaDataOptions; @@ -57,7 +64,7 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { test('if metadata has author alias', async () => { const fileContent = readFileSync(authorAliasInMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -77,7 +84,7 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { test('if metadata has full author data', async () => { const fileContent = readFileSync(fullAuthorInMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -113,7 +120,7 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { metadataOptions.vcsConnector = defaultVCSConnector; const fileContent = readFileSync(authorAliasInMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -133,7 +140,7 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { metadataOptions.vcsConnector = undefined; const fileContent = readFileSync(authorAliasInMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -158,7 +165,7 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { }; const fileContent = readFileSync(authorAliasInMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -176,7 +183,7 @@ describe('getContentWithUpdatedMetadata (Authors)', () => { metadataOptions.vcsConnector = defaultVCSConnector; const fileContent = readFileSync(simpleMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, diff --git a/tests/integrations/services/metadataContributors.test.ts b/tests/integrations/services/metadataContributors.test.ts index 6a239b0f..11879e76 100644 --- a/tests/integrations/services/metadataContributors.test.ts +++ b/tests/integrations/services/metadataContributors.test.ts @@ -1,3 +1,5 @@ +import type {Run} from 'commands/build'; + import {readFileSync} from 'fs'; import {normalize} from 'path'; import {Contributor, Contributors, MetaDataOptions} from 'models'; @@ -37,6 +39,11 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { getModifiedTimeByPath: () => undefined, }; + const run = { + input: '', + realpath: (path: string) => path, + } as unknown as Run; + describe( 'should return file content with updated contributors in metadata ' + 'if metadata options has "isContributorsEnabled" equals true.', @@ -54,7 +61,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { }); const fileContent = readFileSync(simpleMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -77,7 +84,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { }); const fileContent = readFileSync(withoutMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -119,7 +126,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { }); const fileContent = readFileSync(simpleMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -160,7 +167,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { }); const fileContent = readFileSync(simpleMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -247,7 +254,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { metadataOptions.pathData.resolvedPathToFile = withIncludesFilePath as AbsolutePath; const fileContent = readFileSync(withIncludesFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -274,7 +281,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { metadataOptions.vcsConnector = defaultVCSConnector; const fileContent = readFileSync(simpleMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, @@ -294,7 +301,7 @@ describe('getContentWithUpdatedMetadata (Contributors)', () => { metadataOptions.vcsConnector = undefined; const fileContent = readFileSync(simpleMetadataFilePath, 'utf8'); - const updatedFileContent = await enrichWithFrontMatter({ + const updatedFileContent = await enrichWithFrontMatter(run, { fileContent, metadataOptions, resolvedFrontMatterVars: {}, From caa7907aae91909cc9d60f6711b031f2c499fa5f Mon Sep 17 00:00:00 2001 From: 3y3 <3y3@ya.ru> Date: Thu, 2 Jan 2025 09:19:19 +0300 Subject: [PATCH 2/4] deps: Update openapi extension --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 185b0e53..4732ad6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@diplodoc/eslint-config": "^2.0.0", "@diplodoc/latex-extension": "^1.3.2", "@diplodoc/mermaid-extension": "^1.3.2", - "@diplodoc/openapi-extension": "^2.6.0", + "@diplodoc/openapi-extension": "^2.7.0", "@diplodoc/prettier-config": "^2.0.0", "@diplodoc/search-extension": "^1.1.5", "@diplodoc/transform": "^4.40.1", @@ -2259,9 +2259,9 @@ } }, "node_modules/@diplodoc/openapi-extension": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@diplodoc/openapi-extension/-/openapi-extension-2.6.0.tgz", - "integrity": "sha512-SjH954/C52+h+jIZ8hYhFP0Aj6pLIOpv9Am1tzppKzmM0bwnN6II0ko0ui9m0dxbeHxkszcKCFC3vY9/PUg9Kw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@diplodoc/openapi-extension/-/openapi-extension-2.7.0.tgz", + "integrity": "sha512-0eNga9oywvH4udEQ3B6+5Pu2WNyaacIOjmGJIfNAqwwe/h3uYLRlxgfTi0dy1vsWIIrU3mT7IEbv3biezcF5wA==", "dev": true, "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", @@ -2281,8 +2281,8 @@ "peerDependencies": { "@gravity-ui/uikit": "^6.0.0", "markdown-it": "^13.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@diplodoc/prettier-config": { diff --git a/package.json b/package.json index f6352cf2..a083903e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@diplodoc/eslint-config": "^2.0.0", "@diplodoc/latex-extension": "^1.3.2", "@diplodoc/mermaid-extension": "^1.3.2", - "@diplodoc/openapi-extension": "^2.6.0", + "@diplodoc/openapi-extension": "^2.7.0", "@diplodoc/prettier-config": "^2.0.0", "@diplodoc/search-extension": "^1.1.5", "@diplodoc/transform": "^4.40.1", From 68b02aca6bee7c6b7c884cd844cc6118598a905b Mon Sep 17 00:00:00 2001 From: 3y3 <3y3@ya.ru> Date: Thu, 2 Jan 2025 09:30:56 +0300 Subject: [PATCH 3/4] fixup! 09f0781e8a133439b72491d618a2c73b0ba0e2f9 --- src/commands/build/core/toc/loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/build/core/toc/loader.ts b/src/commands/build/core/toc/loader.ts index e34d0d06..42e12cf2 100644 --- a/src/commands/build/core/toc/loader.ts +++ b/src/commands/build/core/toc/loader.ts @@ -169,7 +169,7 @@ async function processItems(this: LoaderContext, toc: RawToc): Promise { const path = include.path.endsWith('toc.yaml') ? include.path : join(include.path, 'toc.yaml'); - const tocPath = join(dirname(this.path), path); + const tocPath = normalizePath(join(dirname(this.path), path)); for (const includer of include.includers) { const hook = this.toc.hooks.Includer.get(includer.name); From 17dad6ec0c32a6c2a313a4295c60ad1583e6a067 Mon Sep 17 00:00:00 2001 From: 3y3 <3y3@ya.ru> Date: Thu, 2 Jan 2025 09:51:58 +0300 Subject: [PATCH 4/4] debug --- tests/utils.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/utils.ts b/tests/utils.ts index 66552301..f63f421e 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -9,8 +9,7 @@ const assets = require('@diplodoc/client/manifest'); export function platformless(text: string) { return text .replace(/\r\n/g, '\n') - .replace(/(\\(?![\/"'])){1,2}/g, '/') - .replace(/(Config|Documentation)-\d+-\d+.\d+/g, '$1-RANDOM'); + .replace(/(\\(?![\/"'])){1,2}/g, '/'); } export function bundleless(text: string) { @@ -63,12 +62,22 @@ export function runYfmDocs(inputPath: string, outputPath: string, {md2md=true, m if (md2md && md2html) { shell.rm('-rf', `${outputPath}-html`); - shell.exec(`node ${yfmDocsPath} --input ${inputPath} --output ${outputPath} --output-format=md --allowHTML --quiet ${args}`); - shell.exec(`node ${yfmDocsPath} --input ${outputPath} --output ${outputPath}-html --allowHTML --quiet ${args}`); + logResult(shell.exec(`node ${yfmDocsPath} --input ${inputPath} --output ${outputPath} --output-format=md --allowHTML --quiet ${args}`)); + logResult(shell.exec(`node ${yfmDocsPath} --input ${outputPath} --output ${outputPath}-html --allowHTML --quiet ${args}`)); } else if (md2md) { - shell.exec(`node ${yfmDocsPath} --input ${inputPath} --output ${outputPath} --output-format=md --allowHTML --quiet ${args}`); + logResult(shell.exec(`node ${yfmDocsPath} --input ${inputPath} --output ${outputPath} --output-format=md --allowHTML --quiet ${args}`)); } else { - shell.exec(`node ${yfmDocsPath} --input ${inputPath} --output ${outputPath} --allowHTML --quiet ${args}`); + logResult(shell.exec(`node ${yfmDocsPath} --input ${inputPath} --output ${outputPath} --allowHTML --quiet ${args}`)); + } +} + +function logResult(result) { + if (result.code > 0) { + console.log('=== STDOUT ==='); + console.log(result.stdout); + console.log('=== STDERR ==='); + console.log(result.stderr); + console.log('=== END ==='); } }