UNPKG

@telefonica/markdown-confluence-sync

Version:

Creates/updates/deletes Confluence pages based on markdown files in a directory. Supports Mermaid diagrams and per-page configuration using frontmatter metadata. Works great with Docusaurus

142 lines (141 loc) 5.6 kB
// SPDX-FileCopyrightText: 2024 Telefónica Innovación Digital // SPDX-License-Identifier: Apache-2.0 import { existsSync, lstatSync, readFileSync } from "node:fs"; import { readdir } from "node:fs/promises"; import { basename, join, relative } from "node:path"; import { parse as parseYaml } from "yaml"; import globule from "globule"; import { InvalidPathException } from "../pages/errors/InvalidPathException.js"; import { PathNotExistException } from "../pages/errors/PathNotExistException.js"; import { IndexFileIgnoreException } from "../pages/errors/IndexFileIgnoreException.js"; import { isValidFile } from "../util/files.js"; import { DocusaurusDocItemFactory } from "./DocusaurusDocItemFactory.js"; import { DocusaurusDocTreePageFactory } from "./DocusaurusDocTreePageFactory.js"; import { CategoryItemMetadataValidator } from "./support/validators/CategoryItemMetadata.js"; export const DocusaurusDocTreeCategory = class DocusaurusDocTreeCategory { _cwd; _path; _index; _meta; _logger; _filesMetadata; _contentPreprocessor; _filesIgnore; constructor(path, options) { if (!existsSync(path)) { throw new PathNotExistException(`Path ${path} does not exist`); } if (!lstatSync(path).isDirectory()) { throw new InvalidPathException(`Path ${path} is not a directory`); } try { this._index = DocusaurusDocTreePageFactory.fromCategoryIndex(path, options); } catch (e) { if (e instanceof PathNotExistException || e instanceof IndexFileIgnoreException) { options?.logger?.warn(e.message); } else { throw e; } } this._cwd = options.cwd; this._path = path; this._meta = DocusaurusDocTreeCategory._processCategoryItemMetadata(path); this._logger = options?.logger; this._filesMetadata = options?.filesMetadata; this._contentPreprocessor = options?.contentPreprocessor; this._filesIgnore = options?.filesIgnore; } get isCategory() { return true; } get meta() { return { title: this._meta?.title ?? this._index?.meta.title ?? basename(this._path), syncToConfluence: this._index?.meta.syncToConfluence ?? true, confluenceShortName: this._index?.meta.confluenceShortName, confluenceTitle: this._index?.meta.confluenceTitle, }; } get content() { return this._index?.content ?? ""; } get path() { // NOTE: fake index.md path to be able reference following the same logic as for pages return this._index?.path ?? join(this._path, "index.md"); } get containsIndex() { return this._index !== undefined; } static _detectCategoryItemFile(path) { if (existsSync(join(path, "_category_.yml"))) { return join(path, "_category_.yml"); } if (existsSync(join(path, "_category_.yaml"))) { return join(path, "_category_.yaml"); } if (existsSync(join(path, "_category_.json"))) { return join(path, "_category_.json"); } return null; } static _processCategoryItemMetadata(path) { const categoryItemFile = DocusaurusDocTreeCategory._detectCategoryItemFile(path); if (categoryItemFile === null) { return undefined; } try { const categoryMeta = parseYaml(readFileSync(categoryItemFile).toString()); const { label } = CategoryItemMetadataValidator.parse(categoryMeta); return { title: label, }; } catch (e) { throw new Error(`Path ${path} has an invalid _category_.yml file`, { cause: e, }); } } async visit() { if (!this.meta.syncToConfluence) { this._logger?.debug(`Category ${this._path} is not set to sync to Confluence`); return []; } const paths = await readdir(this._path); const childrenPaths = paths .map((path) => join(this._path, path)) .filter(this._isDirectoryOrNotIndexFile.bind(this)); const childrenItems = await Promise.all(childrenPaths.map((path) => DocusaurusDocItemFactory.fromPath(path, { cwd: this._cwd, logger: this._logger?.namespace(path.replace(this._path, "")), filesMetadata: this._filesMetadata, contentPreprocessor: this._contentPreprocessor, filesIgnore: this._filesIgnore, }))); const flattenedItems = await Promise.all(childrenItems.map((root) => root.visit())); const items = flattenedItems.flat(); this._logger?.debug(`Category ${this._path} has ${items.length} children`); if (items.length === 0) { return this.containsIndex ? [this] : []; } return [this, ...items]; } _isDirectoryOrNotIndexFile(path) { const isValid = lstatSync(path).isDirectory() || isValidFile(path); if (!isValid) { return false; } // Check if file should be ignored based on filesIgnore pattern if (this._filesIgnore) { const relativePath = relative(this._cwd, path); if (globule.isMatch(this._filesIgnore, relativePath)) { this._logger?.debug(`Ignoring file ${path} based on filesIgnore pattern`); return false; } } return true; } };