@ckeditor/typedoc-plugins
Version:
Various TypeDoc plugins developed by the CKEditor 5 team.
1,158 lines (1,157 loc) • 48.2 kB
JavaScript
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 };