@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
JavaScript
// 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;
}
};