UNPKG

@ckeditor/typedoc-plugins

Version:
1,158 lines (1,157 loc) 48.2 kB
import { ArrayType, Comment, Converter, DeclarationReflection, IntrinsicType, ParameterReflection, ReferenceReflection, ReferenceType, ReflectionFlag, ReflectionKind, TypeScript } from "typedoc"; import fs from "node:fs"; import upath from "upath"; //#region src/utils/getpluginpriority.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const pluginGroups = [ ["typeDocRestoreProgramAfterConversion"], [ "typeDocModuleFixer", "typeDocSymbolFixer", "typeDocReferenceFixer" ], [ "typeDocTagObservable", "typeDocTagError", "typeDocTagEvent" ], [ "typeDocEventInheritanceFixer", "typeDocEventParamFixer", "typeDocInterfaceAugmentationFixer" ], ["typeDocPurgePrivateApiDocs"], ["validators"] ]; /** * Returns the priority of a plugin. To avoid collisions between the built-in TypeDoc plugins, the priority is negative. * * @param pluginName */ function getPluginPriority(pluginName) { for (let i = 0; i < pluginGroups.length; i++) if (pluginGroups[i].includes(pluginName)) { if (i === 0) return 0; return -i; } return -pluginGroups.length; } //#endregion //#region src/module-fixer/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ function typeDocModuleFixer(app) { app.converter.on(Converter.EVENT_CREATE_DECLARATION, onEventCreateDeclaration$1(), getPluginPriority(typeDocModuleFixer.name)); } function onEventCreateDeclaration$1() { return (context, reflection) => { if (reflection.kind !== ReflectionKind.Module) return; const symbol = context.getSymbolFromReflection(reflection); if (!symbol) return; const node = symbol.declarations.at(0); if (!("statements" in node)) return; const { statements } = node; for (const statement of statements) { if (!("jsDoc" in statement)) continue; if (!Array.isArray(statement.jsDoc)) continue; for (const jsDoc of statement.jsDoc) { const moduleTag = (jsDoc.tags || []).find((tag) => { return tag.tagName.text === "module"; }); if (!moduleTag) continue; reflection.name = moduleTag.comment; return; } } }; } //#endregion //#region src/symbol-fixer/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-symbol-fixer` plugin renames `Symbol.*` definitions with the JSDoc style. * * * Typedoc: `[iterator]() → Iterator` * * JSDoc: `Symbol.iterator() → Iterator` */ function typeDocSymbolFixer(app) { app.converter.on(Converter.EVENT_CREATE_DECLARATION, onEventCreateDeclaration(), getPluginPriority(typeDocSymbolFixer.name)); } function onEventCreateDeclaration() { return (context, reflection) => { if (!isWrappedInSquareBrackets(reflection.name)) return; const symbolName = reflection.name.slice(1, -1); if (symbolName in Symbol) reflection.name = `Symbol.${symbolName}`; else { const node = context.getSymbolFromReflection(reflection).declarations.at(0); context.logger.warn("Non-symbol wrapped in square brackets", node); } }; } function isWrappedInSquareBrackets(value) { return value.startsWith("[") && value.endsWith("]"); } //#endregion //#region src/tag-error/errortagserializer.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ var ErrorTagSerializer = class { get priority() { return 0; } supports(item) { return "isCKEditor5Error" in item; } toObject(item, obj, serializer) { obj.isCKEditor5Error = item.isCKEditor5Error; obj.parameters = item.parameters.map((item) => item.toObject(serializer)); return obj; } }; //#endregion //#region src/utils/isreflectionvalid.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Checks if the reflection can be considered as "valid" (supported). Only reflections that are not nested inside a type are supported. */ function isReflectionValid(reflection) { if (reflection.name === "__type") return false; if (reflection.parent) return isReflectionValid(reflection.parent); return true; } //#endregion //#region src/utils/isabsoluteidentifier.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Checks if the identifier is an absolute one. */ function isAbsoluteIdentifier(identifier) { return identifier.startsWith("module:"); } //#endregion //#region src/utils/toabsoluteidentifier.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Converts a relative identifier into an absolute one. */ function toAbsoluteIdentifier(reflection, identifier) { const separator = identifier[0]; const parts = getLongNameParts(reflection); return separator === "~" ? "module:" + parts[0] + identifier : "module:" + parts[0] + "~" + parts[1] + identifier; } /** * Returns a longname for a reflection, divided into separate parts. */ function getLongNameParts(reflection) { const kinds = [ ReflectionKind.Module, ReflectionKind.Class, ReflectionKind.Function, ReflectionKind.Interface, ReflectionKind.TypeAlias, ReflectionKind.Accessor, ReflectionKind.Variable, ReflectionKind.Method, ReflectionKind.Property ]; const parts = []; while (reflection) { if (kinds.includes(reflection.kind)) parts.unshift(reflection.name); reflection = reflection.parent; } return parts; } //#endregion //#region src/utils/gettarget.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Checks if the provided identifier targets an existing reflection within the whole project and returns found reflection. * If the target is not found, returns null. */ function getTarget(context, reflection, identifier) { if (!identifier) return null; const absoluteIdentifier = isAbsoluteIdentifier(identifier) ? identifier : toAbsoluteIdentifier(reflection, identifier); const parts = absoluteIdentifier.substring(7).split(/#|~|\./); const lastPart = parts.pop(); const [lastPartName, lastPartLabel] = lastPart.split(":"); const isIdentifierEvent = lastPart.startsWith("event:"); const isIdentifierLabeledSignature = !isIdentifierEvent && lastPart.includes(":"); let targetReflection; if (isIdentifierEvent) targetReflection = context.project.getChildByName(parts)?.ckeditor5Events.find((event) => { return event.name === lastPart.replace("event:", ""); }); else { parts.push(isIdentifierLabeledSignature ? lastPartName : lastPart); targetReflection = context.project.getChildByName(parts); } if (!targetReflection) return null; if (isIdentifierLabeledSignature) { if (!targetReflection.signatures) return null; return targetReflection.signatures.find((signature) => { if (!signature.comment) return false; const labelTag = signature.comment.getTag("@label"); if (!labelTag) return false; return labelTag.content[0]?.text === lastPartLabel; }) || null; } if (absoluteIdentifier.includes(".") !== targetReflection.flags.isStatic) return null; return targetReflection; } //#endregion //#region src/utils/isidentifiervalid.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Checks if the name (identifier) that is provided for a tag, points to an existing reflection in the whole project. * The identifier can be either a relative or an absolute one. */ function isIdentifierValid(context, reflection, identifier) { if (!identifier) return false; if (reflection.inheritedFrom) return true; return !!getTarget(context, reflection, identifier); } //#endregion //#region src/utils/getnode.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Returns the TypeScript node from the reflection. */ function getNode(context, reflection) { let symbol = null; let declarationIndex = 0; if (reflection.isSignature()) { symbol = context.getSymbolFromReflection(reflection.parent); declarationIndex = reflection.parent.signatures.findIndex((signature) => signature.id === reflection.id); } else symbol = context.getSymbolFromReflection(reflection); if (!symbol) return null; return symbol.declarations[declarationIndex] || null; } //#endregion //#region src/utils/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ //#endregion //#region src/tag-error/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ const ERROR_PARAM_REGEXP = /@param\s+\{([^}]+)\}\s+([\w.]+)\s+(.+)/; const ARRAY_TYPE_REGEXP = /<([^>]+)>/; const ERROR_TAG_NAME = "error"; /** * The `typedoc-plugin-tag-error` collects error definitions from the `@error` tag. */ function typeDocTagError(app) { app.converter.on(Converter.EVENT_END, onEventEnd$8, getPluginPriority(typeDocTagError.name)); app.serializer.addSerializer(new ErrorTagSerializer()); } function onEventEnd$8(context) { const moduleReflections = context.project.getReflectionsByKind(ReflectionKind.Module); for (const reflection of moduleReflections) { const symbol = context.getSymbolFromReflection(reflection); if (!symbol) continue; const nodes = findDescendant(symbol.declarations.at(0).getSourceFile(), (node) => { if (node.kind !== TypeScript.SyntaxKind.Identifier) return false; if (node.text !== ERROR_TAG_NAME) return false; if (node.parent.tagName?.text !== ERROR_TAG_NAME) return false; if (!node.parent.comment) return false; return true; }); for (const errorNode of nodes) { const parentNode = errorNode.parent; const errorName = parentNode.comment; const { parent } = parentNode; const errorDeclaration = new DeclarationReflection(errorName, ReflectionKind.Document, reflection); errorDeclaration.isCKEditor5Error = true; errorDeclaration.comment = createComment(parent.comment); errorDeclaration.parameters = parentNode.parent.getChildren().filter((_childTag) => { const childTagAsParam = _childTag; if (!childTagAsParam.comment || !parent.comment) return false; if (childTagAsParam === parentNode) return false; if (childTagAsParam.tagName.text !== "param") return false; return true; }).map((childTag) => { return createParameter$1(context, errorDeclaration, childTag, errorDeclaration); }); context.withScope(reflection).postReflectionCreation(errorDeclaration, getSymbol(errorNode), void 0); } } } function createParameter$1(context, reflection, childTag, parent) { const match = childTag.getText().match(ERROR_PARAM_REGEXP); const parameter = new ParameterReflection(childTag.name.text, ReflectionKind.Parameter, parent); let isArray = false; try { if (!match) throw new Error("Invalid signature to process."); let typeName = match[1]; const name = match[2]; const description = match[3]; if (name.includes(".")) throw new Error("A dot notation @param is not supported."); parameter.name = name; isArray = typeName.startsWith("Array"); if (isArray) typeName = typeName.match(ARRAY_TYPE_REGEXP).at(1); if (typeName.startsWith("module:")) { const childReflection = getTarget(context, reflection, typeName); if (!childReflection) throw new Error("A module reflection cannot be found."); const returnType = ReferenceType.createResolvedReference(childTag.name.text, childReflection, context.project); parameter.type = isArray ? new ArrayType(returnType) : returnType; } else parameter.type = context.converter.convertType(context, childTag.typeExpression); parameter.comment = createComment(description); } catch { const anyType = new IntrinsicType("any"); parameter.type = isArray ? new ArrayType(anyType) : anyType; parameter.comment = createComment(childTag.comment); } return parameter; } function findDescendant(sourceFileOrNode, callback) { const output = []; for (const node of sourceFileOrNode.getChildren()) { const nodeAsErrorTag = node; if (nodeAsErrorTag.getChildCount()) output.push(...findDescendant(nodeAsErrorTag, callback)); else if (callback(nodeAsErrorTag)) output.push(nodeAsErrorTag); } return output; } /** * Converts a comment node (or array of nodes) to a format expected by `Reflection#comment`. */ function createComment(commentChildrenOrValue) { if (!commentChildrenOrValue) return new Comment([]); if (typeof commentChildrenOrValue === "string") return new Comment([{ kind: "text", text: commentChildrenOrValue }]); return new Comment(commentChildrenOrValue.map((item) => { if (item.kind === TypeScript.SyntaxKind.JSDocLink) { let { text } = item; if ("name" in item && item.name) { const { name } = item; text = name.text + text; } return { text, kind: "inline-tag", tag: "@link" }; } return { text: item.text, kind: "text" }; }).filter(({ text }) => text.length)); } function getSymbol(node) { return ("symbol" in node ? node.symbol : null) || getSymbol(node.parent); } //#endregion //#region src/tag-event/eventtagserializer.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ var EventTagSerializer = class { get priority() { return 0; } supports(item) { return "ckeditor5Events" in item; } toObject(item, obj, serializer) { obj.ckeditor5Events = item.ckeditor5Events.map((internalItem) => { const itemObj = internalItem.toObject(serializer); itemObj.parameters = internalItem.parameters.map((internalParam) => internalParam.toObject(serializer)); return itemObj; }); return obj; } }; //#endregion //#region src/tag-event/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-tag-event` collects event definitions from the `@eventName` tag and assigns them as the children of the class or * the `Observable` interface. * * We are not using the `@event` tag, known from the JSDoc specification, because it has a special meaning in the TypeDoc, and it would be * difficult to get it to work as we expect. This is why we have introduced a support for the custom `@eventName` tag, which now replaces * the old `@event` tag. * * To correctly define an event, it must be associated with the exported type that describes that event, with the `name` and `args` * properties. The value for the `@eventName` tag must be a valid link to a class or to an interface, either a relative or an absolute one. * * To correctly define the event parameters, they must be defined in the `args` property. The `args` property is an array, where each item * describes a parameter that event emits. Item can be either of a primitive type, or a custom type that has own definition. * * Example: * * ```ts * export class ExampleClass {} * * export type ExampleType = { * name: string; * }; * * /** * * An event associated with exported type. * * * * @eventName ~ExampleClass#foo-event * * @param p1 Description for first param. * * @param p2 Description for second param. * * @param p3 Description for third param. * * / * export type FooEvent = { * name: string; * args: [ * p1: string; * p2: number; * p3: ExampleType; * ]; * }; * ``` * * Exported type may contain multiple `@eventName` tags to re-use the same type to create many different events. */ function typeDocTagEvent(app) { app.converter.on(Converter.EVENT_END, onEventEnd$7, getPluginPriority(typeDocTagEvent.name)); app.serializer.addSerializer(new EventTagSerializer()); } function onEventEnd$7(context) { const eventKind = ReflectionKind.TypeAlias; const reflections = context.project.getReflectionsByKind(eventKind); for (const reflection of reflections) { if (!reflection.comment || !reflection.comment.getTag("@eventName")) continue; const eventTags = reflection.comment.getTags("@eventName").map((tag) => tag.content.at(0).text); for (const eventTag of eventTags) { const [eventOwner, eventName] = eventTag.split("#"); const ownerReflection = getTarget(context, reflection, eventOwner); if (!isClassOrInterface(ownerReflection)) { const node = context.getSymbolFromReflection(reflection).declarations[0]; context.logger.warn(`Skipping unsupported "${eventTag}" event.`, node); continue; } const eventReflection = createNewEventReflection$1(context, reflection, eventName); eventReflection.parent = ownerReflection; ownerReflection.ckeditor5Events ??= []; ownerReflection.ckeditor5Events.push(eventReflection); } } } /** * Checks if the found owner reflection for an event is either an interface or a class. */ function isClassOrInterface(reflection) { if (!reflection) return false; if (reflection.kind !== ReflectionKind.Class && reflection.kind !== ReflectionKind.Interface) return false; return true; } /** * Creates new reflection for the provided event name, in the event owner's scope. */ function createNewEventReflection$1(context, sourceReflection, eventName) { const eventReflection = new DeclarationReflection(normalizeEventName(eventName), ReflectionKind.Document); eventReflection.isCKEditor5Event = true; const paramTags = sourceReflection.comment.getTags("@param"); eventReflection.parameters = getArgsTuple(sourceReflection).map((arg, index) => { let argName; if (arg.type === "namedTupleMember") argName = arg.name; else if (paramTags[index]) argName = paramTags[index].name; else argName = "<anonymous>"; const param = new ParameterReflection(argName, ReflectionKind.Parameter, eventReflection); param.type = arg; if (arg.type === "namedTupleMember" && arg.isOptional || param.type.type === "optional") param.setFlag(ReflectionFlag.Optional); const comment = paramTags.find((tag) => tag.name === argName); if (comment) param.comment = new Comment(comment.content); return param; }); eventReflection.comment = sourceReflection.comment.clone(); eventReflection.sources = [...sourceReflection.sources]; context.postReflectionCreation(eventReflection, context.getSymbolFromReflection(sourceReflection), void 0); return eventReflection; } /** * Tries to find the `args` tuple, that is associated with the event, and it contains all event parameters. */ function getArgsTuple(reflection) { const argsTuple = [ ...getTypeArgumentsFromReflection(reflection), ...getChildrenFromReflection(reflection), reflection.type ].flatMap((type) => getTargetTypeReflections(type)).flatMap((type) => { if (type.type === "reflection") return type.declaration.children || []; return type; }).find((property) => property.name === "args"); if (!argsTuple) return []; const tupleType = argsTuple.type; if (!tupleType.elements) return [new IntrinsicType("any")]; return tupleType.elements; } function getTypeArgumentsFromReflection(reflection) { if (!reflection.type) return []; if (reflection.type.type === "reference") return reflection.type.typeArguments || []; return []; } function getChildrenFromReflection(reflection) { return reflection.children || []; } /** * Returns all type reflections for the specified reflection. * * If the type reflection is a reference to another one, it recursively walks deep until the final declaration is reached. * If the type reflection is an intersection, all member reflections are recursively checked. */ function getTargetTypeReflections(reflectionType) { if (!reflectionType) return []; if (reflectionType.type === "reference") { if (!reflectionType.reflection) return []; const reflection = reflectionType.reflection; if (!reflection.type) return reflection.children || []; return getTargetTypeReflections(reflection.type); } if (reflectionType.type === "intersection") return reflectionType.types.flatMap((type) => getTargetTypeReflections(type)); return [reflectionType]; } /** * Returns the normalized event name to make sure that it always starts with the "event:" prefix. */ function normalizeEventName(eventName) { if (eventName.startsWith("event:")) return eventName.replace("event:", ""); return eventName; } //#endregion //#region src/tag-observable/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-tag-observable` handles the `@observable` tag that is assigned to the class property. If found, two new events are * created and inserted as a class children: `change:{property}` and `set:{property}`, where `{property}` is the name of the observable * class property. */ function typeDocTagObservable(app) { app.converter.on(Converter.EVENT_END, onEventEnd$6, getPluginPriority(typeDocTagObservable.name)); } function onEventEnd$6(context) { const kinds = ReflectionKind.Property | ReflectionKind.GetSignature | ReflectionKind.SetSignature; const reflections = context.project.getReflectionsByKind(kinds); for (const reflection of reflections) { if (!reflection.comment || !reflection.comment.getTag("@observable")) continue; /** * Otherwise, if the property has the `@observable` tag, get its name and its parent class. * Accessor reflections need to use their grandparent instead, otherwise it will be: * * `module:core/editor/editor~Editor#isReadOnly` * * instead of: * * `module:core/editor/editor~Editor` */ const propertyName = reflection.name; const ownerReflection = reflection.kind === ReflectionKind.Property ? reflection.parent : reflection.parent.parent; for (const eventName of ["change", "set"]) { const eventReflection = createNewEventReflection(context, { reflection, eventName, propertyName }); eventReflection.parent = ownerReflection; ownerReflection.ckeditor5Events ??= []; ownerReflection.ckeditor5Events.push(eventReflection); } } } /** * Creates new reflection for the provided event name. */ function createNewEventReflection(context, options) { const { reflection, eventName, propertyName } = options; const eventReflection = context.createDeclarationReflection(ReflectionKind.Document, void 0, void 0, `${eventName}:${propertyName}`); eventReflection.isCKEditor5Event = true; eventReflection.parameters = [ createParameter(context, { name: "name", parent: eventReflection, kind: TypeScript.SyntaxKind.StringKeyword, comment: `Name of the changed property (\`${propertyName}\`).` }), createParameter(context, { name: "value", parent: eventReflection, type: reflection.type, comment: `New value of the \`${propertyName}\` property with given key or \`null\`, if operation should remove property.` }), createParameter(context, { name: "oldValue", parent: eventReflection, type: reflection.type, comment: `Old value of the \`${propertyName}\` property with given key or \`null\`, if property was not set before.` }) ]; const modifierTags = new Set(reflection.comment?.modifierTags); eventReflection.comment = new Comment([{ kind: "text", text: eventName === "change" ? `Fired when the \`${propertyName}\` property changed value.` : `Fired when the \`${propertyName}\` property is going to be set but is not set yet (before the \`change\` event is fired).` }], [], modifierTags); eventReflection.sources = [...reflection.sources]; if (reflection.inheritedFrom) eventReflection.inheritedFrom = reflection.inheritedFrom; return eventReflection; } /** * Creates and returns new parameter reflection. */ function createParameter(context, options) { const parameter = new ParameterReflection(options.name, ReflectionKind.Parameter, options.parent); if (options.type) parameter.type = options.type; else { const type = { kind: options.kind }; parameter.type = context.converter.convertType(context, type); } parameter.comment = new Comment([{ kind: "text", text: options.comment }]); return parameter; } //#endregion //#region src/event-param-fixer/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-event-param-fixer` creates the `eventInfo` parameter that is of type `EventInfo` class, and then inserts it as the * first parameter for each found event reflection. */ function typeDocEventParamFixer(app) { app.converter.on(Converter.EVENT_END, onEventEnd$5, getPluginPriority(typeDocEventParamFixer.name)); } function onEventEnd$5(context) { const eventInfoClass = context.project.getChildByName(["utils/eventinfo", "EventInfo"]); if (!eventInfoClass) { context.logger.warn("Unable to find the \"EventInfo\" class."); return; } const eventInfoClassReference = ReferenceType.createResolvedReference("EventInfo", eventInfoClass, context.project); const events = context.project.getReflectionsByKind(ReflectionKind.Class | ReflectionKind.Interface).flatMap((ref) => ref.ckeditor5Events || []); for (const eventReflection of events) { const eventInfoParameter = new ParameterReflection("eventInfo", ReflectionKind.TypeReferenceTarget, eventReflection); eventInfoParameter.type = eventInfoClassReference; eventInfoParameter.comment = new Comment([{ kind: "text", text: "An object containing information about the fired event." }]); eventReflection.parameters = eventReflection.parameters || []; eventReflection.parameters.unshift(eventInfoParameter); } } //#endregion //#region src/event-inheritance-fixer/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-event-inheritance-fixer` takes care of inheriting events, which are not handled by TypeDoc by default. * * Event can be inherited from a class or from an interface. If a class or an interface fires an event, and it is a base for another * class or interface, then all events from the base reflection are copied and inserted into each derived reflection. * * The plugin takes care of events that are specified in parent classes too. */ function typeDocEventInheritanceFixer(app) { app.converter.on(Converter.EVENT_END, onEventEnd$4, getPluginPriority(typeDocEventInheritanceFixer.name)); } function onEventEnd$4(context) { const eventKind = ReflectionKind.Class | ReflectionKind.Interface; const reflections = context.project.getReflectionsByKind(eventKind); for (const reflection of reflections) { const eventReflections = getParentClasses(context, reflection).filter(Boolean).flatMap((ref) => ref.ckeditor5Events || []); if (!eventReflections.length) continue; const derivedReflections = [reflection, ...getDerivedReflections(reflection)]; for (const derivedReflection of derivedReflections) for (const eventReflection of eventReflections) { if (derivedReflection.ckeditor5Events?.some((existingEventReflection) => existingEventReflection.name === eventReflection.name)) continue; const clonedEventReflection = context.createDeclarationReflection(ReflectionKind.Document, void 0, void 0, eventReflection.name); clonedEventReflection.isCKEditor5Event = true; clonedEventReflection.parent = derivedReflection; clonedEventReflection.comment = eventReflection.comment?.clone(); clonedEventReflection.sources = [...eventReflection.sources || []]; clonedEventReflection.inheritedFrom = ReferenceType.createResolvedReference(`${eventReflection.parent.name}.${eventReflection.name}`, eventReflection, context.project); if (eventReflection.parameters) clonedEventReflection.parameters = [...eventReflection.parameters]; derivedReflection.ckeditor5Events ??= []; derivedReflection.ckeditor5Events.push(clonedEventReflection); } } } /** * Finds all derived classes and interfaces from the specified base reflection. It traverses the whole inheritance chain. * If the base reflection is not extended or implemented by any other reflection, an empty array is returned. */ function getDerivedReflections(reflection) { const extendedBy = reflection.extendedBy || []; const implementedBy = reflection.implementedBy || []; return [...extendedBy, ...implementedBy].filter((entry) => entry.reflection).flatMap((entry) => { const derivedReflection = entry.reflection; return [derivedReflection, ...getDerivedReflections(derivedReflection)]; }); } /** * Finds all parent classes from the specified base reflection. It traverses the whole inheritance chain. * If the base reflection is not extending, an empty array is returned. */ function getParentClasses(context, reflection) { return (reflection.extendedTypes || []).flatMap((entry) => getDirectParentClasses(context, reflection, entry)).flatMap((parent) => { return [...getParentClasses(context, parent), parent]; }); } function getDirectParentClasses(context, reflection, type) { if (isIntersectionType(type)) return type.types.filter(isReferenceType).map((type) => type.reflection); const parentReflection = type.reflection; if (parentReflection) return [parentReflection]; const aliasDeclaration = getUnresolvedBaseDeclaration(context, reflection); return aliasDeclaration ? getConstructorReturnTypeReflections(context, aliasDeclaration) : []; } function getUnresolvedBaseDeclaration(context, reflection) { const baseExpression = (context.getSymbolFromReflection(reflection)?.declarations?.find(TypeScript.isClassDeclaration))?.heritageClauses?.find((clause) => clause.token === TypeScript.SyntaxKind.ExtendsKeyword)?.types[0]?.expression; return (baseExpression && context.getSymbolAtLocation(baseExpression))?.declarations?.find(TypeScript.isVariableDeclaration) || null; } function getConstructorReturnTypeReflections(context, aliasDeclaration) { return context.checker.getTypeAtLocation(aliasDeclaration.name).getConstructSignatures().map((signature) => context.checker.getReturnTypeOfSignature(signature)).flatMap((type) => getReflectionsFromType(context, type)); } function getReflectionsFromType(context, type) { if (type.isIntersection()) return type.types.flatMap((type) => getReflectionsFromType(context, type)); const symbol = type.getSymbol() || type.aliasSymbol; const resolvedSymbol = symbol && context.resolveAliasedSymbol(symbol); const reflection = resolvedSymbol && context.getReflectionFromSymbol(resolvedSymbol); return reflection instanceof DeclarationReflection ? [reflection] : []; } function isIntersectionType(type) { return type.type === "intersection"; } function isReferenceType(type) { return type.type === "reference"; } //#endregion //#region src/interface-augmentation-fixer/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-interface-augmentation-fixer` tries to fix an interface, that has been extended (augmented) from the outside (from * another module) in the re-exported "index.ts" file. When the extending "declare module ..." declaration contains the full package name, * it points to the "index.ts" file instead of the actual source file. In such case Typedoc adds new properties to the interface, but not * in place where it has been defined, but in the "index.ts" file, where it is re-exported. Then, the generated output contains duplicated * interface definitions, with only one containing all properties. * * This plugin works as follows: * - Copies the externally added properties to the source definition. * - Replaces the duplicated interface with a reference to the source definition. */ function typeDocInterfaceAugmentationFixer(app) { app.converter.on(Converter.EVENT_END, onEventEnd$3, getPluginPriority(typeDocInterfaceAugmentationFixer.name)); } function onEventEnd$3(context) { const reflections = context.project.getReflectionsByKind(ReflectionKind.Interface); for (const reflection of reflections) { const moduleName = reflection.parent.name.split("/").shift(); const interfaceToCopy = context.project.getChildByName([moduleName, reflection.name]); if (!interfaceToCopy) continue; if (!interfaceToCopy.children) continue; if (reflection.id === interfaceToCopy.id) continue; reflection.children = interfaceToCopy.children.slice(); const newRef = new ReferenceReflection(interfaceToCopy.name, reflection, interfaceToCopy.parent); newRef.sources = [...interfaceToCopy.sources || []]; newRef.id = interfaceToCopy.id; context.withScope(newRef.parent).addChild(newRef); const oldSymbol = context.project.getSymbolIdFromReflection(interfaceToCopy); context.project.removeReflection(interfaceToCopy); context.project.registerReflection(newRef.getTargetReflectionDeep(), oldSymbol, void 0); context.project.ckeditor5AugmentedInterfaces ??= []; context.project.ckeditor5AugmentedInterfaces.push(reflection); } } //#endregion //#region src/purge-private-api-docs/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-purge-private-api-docs` removes reflections collected from private packages. * It also removes reflections sources from augmented interfaces which are collected from private packages. * * Private packages are marked with the `private: true` property in their `package.json` files. * * We do not want to expose private APIs in the documentation, but the paid features may extend the configuration reflection. * Add the `@publicApi` annotation to publish a private reflection within the block comment defining a module name. */ function typeDocPurgePrivateApiDocs(app) { app.converter.on(Converter.EVENT_END, onEventEnd$2, getPluginPriority(typeDocPurgePrivateApiDocs.name)); } function onEventEnd$2(context) { const privateModuleReflections = context.project.getReflectionsByKind(ReflectionKind.Module).filter((reflection) => { if (!reflection.sources) return false; const fullFileName = reflection.sources[0].fullFileName; return isPrivatePackageFile(fullFileName); }); for (const reflection of privateModuleReflections) if (!isPublicApi(context.getSymbolFromReflection(reflection).declarations.at(0))) { reflection.getChildrenByKind(ReflectionKind.Class | ReflectionKind.Interface).flatMap((child) => child.ckeditor5Events).filter(Boolean).forEach((reflectionEvent) => { context.project.removeReflection(reflectionEvent); }); context.project.removeReflection(reflection); } else { removePrivateUrlSourcesFromReflection(reflection); removeNonPublicMembersFromReflection(reflection, context); } const augmentedInterfaces = context.project.ckeditor5AugmentedInterfaces || []; for (const reflection of augmentedInterfaces) removePrivateUrlSourcesFromReflection(reflection); } function removeNonPublicMembersFromReflection(reflection, context) { reflection.traverse((child) => { const childReflection = child; removeNonPublicMembersFromReflection(childReflection, context); if (isNonPublicReflection(childReflection)) { if (!childReflection.inheritedFrom) context.project.removeReflection(childReflection); else if (isInheritedReflectionFromPrivatePackage(childReflection)) context.project.removeReflection(childReflection); } if (childReflection.kind === ReflectionKind.Method || childReflection.kind === ReflectionKind.Accessor) { const signatures = childReflection.kind === ReflectionKind.Method ? childReflection.signatures : [childReflection.getSignature, childReflection.setSignature].filter(Boolean); if (!signatures || !signatures.length) context.project.removeReflection(childReflection); } }); } function isNonPublicReflection(reflection) { return reflection.flags.isPrivate || reflection.flags.isProtected || hasInternalTag(reflection); } function hasInternalTag(reflection) { if (!reflection) return false; if (!reflection.comment) return false; if (!reflection.comment.modifierTags) return false; return reflection.comment.modifierTags.has("@internal"); } function isInheritedReflectionFromPrivatePackage(reflection) { return isPrivatePackageFile(reflection.sources[0].fullFileName); } function removePrivateUrlSourcesFromReflection(reflection) { if (reflection.sources) reflection.sources.filter((source) => isPrivatePackageFile(source.fullFileName)).forEach((source) => { delete source.url; }); reflection.traverse((childReflection) => { removePrivateUrlSourcesFromReflection(childReflection); }); } function isPrivatePackageFile(fileName) { let dirName = upath.dirname(fileName); while (dirName !== upath.dirname(dirName)) { const pathToPackageJson = upath.join(dirName, "package.json"); if (fs.existsSync(pathToPackageJson)) { const packageJson = fs.readFileSync(pathToPackageJson).toString(); return !!JSON.parse(packageJson).private; } dirName = upath.dirname(dirName); } throw new Error(`${fileName} is not placed inside a npm package.`); } function isPublicApi(node) { return node.statements.some((statement) => { if (!("jsDoc" in statement)) return false; if (!Array.isArray(statement.jsDoc)) return false; return statement.jsDoc.some((jsDoc) => { if (!jsDoc.tags) return false; return jsDoc.tags.some((tag) => { if (tag.tagName.kind !== TypeScript.SyntaxKind.Identifier) return false; if (tag.tagName.text !== "publicApi") return false; return true; }); }); }); } //#endregion //#region src/restore-program-after-conversion/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-restore-program-after-conversion` restores TypeScript program used for source conversion. * * TypeDoc at some point, after the source compilation and conversion is finished, deletes the `context.program` property. * This operation prevents from using custom TypeDoc plugins, which listen to `EVENT_END` event, because some TypeDoc internals * require that `context.program` exists. */ function typeDocRestoreProgramAfterConversion(app) { app.converter.on(Converter.EVENT_END, onEventEnd$1, getPluginPriority(typeDocRestoreProgramAfterConversion.name)); } function onEventEnd$1(context) { context.setActiveProgram(context.programs.at(0)); } //#endregion //#region src/reference-fixer/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-reference-fixer` tries to fix a case when TypeDoc incorrectly assigns reflections that are re-exported from another * file as a default export. In such case TypeDoc adds reflection declaration to the file containing the re-export and the actual source * file contains only a reference targeting the declaration from the re-exported file. * * This plugin works as follows: * - It searches for reflection declarations defined in "index" modules. Then, all such reflections are moved to the correct place where * they are defined in the source code. References are moved to "index" modules where they are re-exported. * - The exception is the `ckeditor5-icons` package, which contains only references in "index" module. For this case, reflection * declarations which represent icons are created manually. */ function typeDocReferenceFixer(app) { app.converter.on(Converter.EVENT_END, onEventEnd, getPluginPriority(typeDocReferenceFixer.name)); } function onEventEnd(context) { const reflections = context.project.getReflectionsByKind(ReflectionKind.Reference); for (const reflection of reflections) { if (isTypingsSource(reflection)) continue; const reflectionParent = reflection.parent; const targetReflection = reflection.getTargetReflectionDeep(); const targetReflectionParent = targetReflection.parent; if (reflectionParent.name === "icons/index") { const newReflectionName = new DeclarationReflection(reflection.name, ReflectionKind.Variable, reflectionParent); newReflectionName.sources = [...reflection.sources]; newReflectionName.flags = targetReflection.flags; newReflectionName.type = targetReflection.type; reflectionParent.addChild(newReflectionName); reflectionParent.removeChild(reflection); continue; } if (isIndexModule(targetReflectionParent)) { targetReflection.name = targetReflection.escapedName || targetReflection.name; targetReflection.parent = reflectionParent; reflectionParent.addChild(targetReflection); targetReflectionParent.removeChild(targetReflection); reflection.parent = targetReflectionParent; reflectionParent.removeChild(reflection); targetReflectionParent.addChild(reflection); } } } function isTypingsSource(reflection) { const [source] = reflection.sources; return source?.fullFileName.endsWith(".d.ts"); } function isIndexModule(reflection) { const [source] = reflection.sources; const isIndexFile = source.fullFileName.endsWith("/index.ts"); const isModule = reflection.kind === ReflectionKind.Module; return isIndexFile && isModule; } //#endregion //#region src/output-cleanup/cleanupserializer.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ var CleanUpSerializer = class { get priority() { return 0; } supports() { return true; } toObject(item, obj) { delete obj.groups; return obj; } }; //#endregion //#region src/output-cleanup/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * The `typedoc-plugin-output-cleanup` removes unnecessary properties from reflections to decrease the size of the generated output. */ function typeDocOutputCleanUp(app) { app.serializer.addSerializer(new CleanUpSerializer()); } //#endregion //#region src/validators/see-validator/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Validates the output produced by TypeDoc. * * It checks if the identifier in the "@see" tag points to an existing doclet. */ function see_validator_default(context, onError) { const reflections = context.project.getReflectionsByKind(ReflectionKind.All | ReflectionKind.Document).filter(isReflectionValid); for (const reflection of reflections) { const identifiers = getIdentifiersFromSeeTag(reflection); if (!identifiers.length) continue; for (const identifier of identifiers) if (!isIdentifierValid(context, reflection, identifier)) onError(`Incorrect link: "${identifier}"`, getNode(context, reflection)); } } function getIdentifiersFromSeeTag(reflection) { if (!reflection.comment) return []; return reflection.comment.getTags("@see").flatMap((tag) => tag.content.map((item) => item.text.trim())).filter((text) => { if (text.length <= 1) return false; if (/^https?:\/\//.test(text)) return false; return true; }); } //#endregion //#region src/validators/link-validator/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Validates the output produced by TypeDoc. * * It checks if the identifier in the "@link" tag points to an existing doclet. */ function link_validator_default(context, onError) { const reflections = context.project.getReflectionsByKind(ReflectionKind.All | ReflectionKind.Document).filter(isReflectionValid); for (const reflection of reflections) { const identifiers = getIdentifiersFromLinkTag(reflection); if (!identifiers.length) continue; for (const identifier of identifiers) if (!isIdentifierValid(context, reflection, identifier)) onError(`Incorrect link: "${identifier}"`, getNode(context, reflection)); } } function getIdentifiersFromLinkTag(reflection) { if (!reflection.comment) return []; return [...reflection.comment.summary, ...reflection.comment.blockTags.flatMap((tag) => tag.content)].filter((part) => part.kind === "inline-tag" && part.tag === "@link").map((part) => { const [identifier] = part.text.split(" "); return identifier; }); } //#endregion //#region src/validators/fires-validator/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Validates the output produced by TypeDoc. * * It checks if the event in the "@fires" tag exists. */ function fires_validator_default(context, onError) { const reflections = context.project.getReflectionsByKind(ReflectionKind.Class | ReflectionKind.CallSignature).filter(isReflectionValid); for (const reflection of reflections) { const identifiers = getIdentifiersFromFiresTag(reflection); if (!identifiers.length) continue; for (const identifier of identifiers) if (!isIdentifierValid(context, reflection, identifier)) onError(`Incorrect event name: "${identifier.replace(/^#event:/, "")}" in the @fires tag`, getNode(context, reflection)); } } function getIdentifiersFromFiresTag(reflection) { if (!reflection.comment) return []; return reflection.comment.getTags("@fires").flatMap((tag) => tag.content.map((item) => item.text.trim())).map((identifier) => { if (isAbsoluteIdentifier(identifier)) return identifier; return "#event:" + identifier; }); } //#endregion //#region src/validators/overloads-validator/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Validates the output produced by TypeDoc. * * It checks if overloaded methods and functions are described with the mandatory "@label" tag. * * Also, it prevents using the same name twice for overloaded structures. */ function overloads_validator_default(context, onError) { const kinds = ReflectionKind.Method | ReflectionKind.Constructor | ReflectionKind.Function; const reflections = context.project.getReflectionsByKind(kinds).filter(isReflectionValid); for (const reflection of reflections) { if (reflection.signatures.length === 1) continue; const uniqueValues = /* @__PURE__ */ new Set(); const errorMessageSuffix = !!reflection.inheritedFrom ? " due the inherited structure" : ""; for (const signature of reflection.signatures) { const labelTag = signature.comment?.getTag("@label"); const node = getNode(context, signature); if (labelTag) { const comment = labelTag.content.at(0); if (uniqueValues.has(comment.text)) onError(`Duplicated name: "${comment.text}" in the @label tag` + errorMessageSuffix, node); else uniqueValues.add(comment.text); } else onError("Overloaded signature misses the @label tag" + errorMessageSuffix, node); } } } //#endregion //#region src/validators/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ /** * Validates the CKEditor 5 documentation. */ function validate(app, options = {}) { app.converter.on(Converter.EVENT_END, (context) => { const validators = [ see_validator_default, link_validator_default, fires_validator_default ]; if (options.enableOverloadValidator) validators.push(overloads_validator_default); app.logger.info("Starting validation..."); const errors = /* @__PURE__ */ new Map(); validators.forEach((validator) => { validator(context, (error, node) => { errors.set(node, { error, node }); }); }); errors.forEach(({ error, node }) => app.logger.warn(error, node)); app.logger.info("Validation completed."); if (options.strict && errors.size) throw "Found errors during the validation process."; }, getPluginPriority("validators")); } //#endregion //#region src/index.ts /** * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md. */ //#endregion export { typeDocEventInheritanceFixer, typeDocEventParamFixer, typeDocInterfaceAugmentationFixer, typeDocModuleFixer, typeDocOutputCleanUp, typeDocPurgePrivateApiDocs, typeDocReferenceFixer, typeDocRestoreProgramAfterConversion, typeDocSymbolFixer, typeDocTagError, typeDocTagEvent, typeDocTagObservable, validate };