blob: 4335cce541892e71729fb14bdba41b88a7b4af82 [file] [edit]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: lines_longer_than_80_chars
import 'dart:io';
import 'package:cli_util/cli_logging.dart';
import 'package:path/path.dart' as p;
import '../api.dart';
import '../workspace.dart';
import 'markdown.dart';
import 'render.dart';
import 'utils.dart';
class Generator {
final Workspace workspace;
final Directory outDir;
final Logger logger;
final Stats? stats;
Generator({
required this.workspace,
required this.outDir,
required this.logger,
this.stats,
});
Future<void> generate() async {
// write out the static resources
await workspace.htmlTemplate.generateStaticResources(outDir, stats: stats);
// mainFile
if (workspace.mainFile != null) {
await _generateFile(workspace.mainFile!);
}
// navFiles
for (var navElement in workspace.navFiles) {
await _generateFile(navElement, printProgress: false);
}
// children
for (var child in workspace.children) {
if (child is WorkspaceSeparator) {
// nothing to generate
} else if (child is WorkspaceFile) {
await _generateFile(child, printProgress: !child.isMarkdown);
} else if (child is WorkspaceDirectory) {
await _generateContainer(child);
} else {
throw StateError('unexpected type: $child');
}
}
// left nav data file
var navFile = File(p.join(outDir.path, '_resources', 'nav.json'));
navFile.writeAsStringSync(workspace.generateNavData());
stats?.genFile(navFile);
// search index file
var indexFile = File(p.join(outDir.path, '_resources', 'index.json'));
indexFile.writeAsStringSync(workspace.api.index.toJson());
stats?.genFile(indexFile);
}
Future<void> _generateContainer(WorkspaceDirectory container) async {
// mainFile
// todo: always generate an index file
if (container.mainFile != null) {
await _generateFile(container.mainFile!);
}
// children
for (var child in container.children) {
if (child is WorkspaceSeparator) {
// nothing to generate
} else if (child is WorkspaceFile) {
await _generateFile(child, printProgress: false);
} else if (child is WorkspaceDirectory) {
await _generateContainer(child);
} else {
throw StateError('unexpected type: $child');
}
}
}
Future<void> _generateFile(
WorkspaceFile file, {
bool printProgress = true,
}) async {
var pageContents = await file.generatePageContents();
var fileContents = await workspace.generateWorkspacePage(
file,
pageContents,
);
var outFile = File(p.join(outDir.path, file.path));
outFile.parent.createSync(recursive: true);
outFile.writeAsStringSync(fileContents);
stats?.genFile(outFile);
if (printProgress) logger.stdout(' ${p.relative(outFile.path)}');
}
}
FileContentGenerator libraryGenerator(LibraryItemContainer library) {
return (Workspace workspace, WorkspaceFile thisFile) async {
return _LibraryGenerator(
library: library,
workspace: workspace,
thisFile: thisFile,
).generate();
};
}
FileContentGenerator itemsGenerator(Items items) {
if (items is InterfaceElementItems) {
return (Workspace workspace, WorkspaceFile thisFile) async {
return _InterfaceElementGenerator(
classItems: items,
workspace: workspace,
thisFile: thisFile,
).generate();
};
} else if (items is ExtensionElementItems) {
return (Workspace workspace, WorkspaceFile thisFile) async {
return _ExtentionElementGenerator(
extensionItems: items,
workspace: workspace,
thisFile: thisFile,
).generate();
};
} else {
throw StateError('unhandled type: $items');
}
}
class _LibraryGenerator {
final LibraryItemContainer library;
final Workspace workspace;
final WorkspaceFile thisFile;
_LibraryGenerator({
required this.library,
required this.workspace,
required this.thisFile,
});
GenerationResults generate() {
var api = workspace.api;
var linkedCodeRenderer = LinkedCodeRenderer(api.resolver, thisFile);
var buf = StringBuffer();
if (thisFile.importScript != null) {
buf.writeln('<h1>${thisFile.name}</h1>');
buf.writeln(
'<pre class="declaration">'
"<code>import '${thisFile.importScript}';</code>"
'</pre>',
);
} else {
buf.writeln('<h1>${thisFile.name}</h1>');
}
if (library.docs != null) {
// todo: also support compound references - Class.field, Class.method
buf.writeln(
convertMarkdown(
library.docs!,
linkResolver: library.markdownLinkResolver(api.resolver),
),
);
}
var pageItemRenderer = OutlineRenderer();
if (library.exports.isNotEmpty) {
buf.writeln('<h2 id="_Exports">Exports</h2>');
for (var entry in library.exportsByLibrary.entries) {
var library = entry.key;
var exports = entry.value;
buf.writeln('<table>');
buf.write('<tr><td colspan=2 class="item-title">');
buf.write('Exports from ${library.urlName}');
buf.writeln('</td></tr>');
var itemsByGroup = Items.itemsByGroup(exports);
for (var entry in itemsByGroup.entries) {
var groupType = entry.key;
var items = entry.value;
buf.write('<tr>');
buf.write('<td class="item-title">${groupType.title}</td>');
buf.write('<td class="item-docs">');
buf.writeln(
items
.map((item) {
var ref = api.hrefOrSpan(
pageItemRenderer.render(item.type, item),
item.element,
from: thisFile,
);
return '<code>$ref</code>';
})
.join(',\n'),
);
buf.write('</td>');
buf.writeln('</tr>');
}
buf.writeln('</table>');
}
}
for (var group in library.groups.values) {
buf.writeln('<h2 id="${group.anchorId}">${group.name}</h2>');
if (group.containerType) {
buf.writeln('<table>');
for (var item in group.items) {
buf.write('<tr><td>');
buf.writeln(
api.hrefOrSpan(
pageItemRenderer.render(group.type, item),
item.element,
from: thisFile,
),
);
buf.write('</td>');
buf.write('<td class="item-docs">');
if (item.docs != null) {
buf.writeln(
convertMarkdown(
firstSentence(item.docs!),
linkResolver: item.markdownLinkResolver(api.resolver),
),
);
}
buf.write('</td></tr>');
}
buf.writeln('</table>');
} else {
for (var item in group.items) {
buf.writeln(
'<h3 id="${item.anchorId}">${pageItemRenderer.render(group.type, item)}'
'<span class="symbol-type">${item.element.kind.displayName}</span>'
'</h3>',
);
buf.write(writeAnnotations(item));
buf.writeln(linkedCodeRenderer.render(group.type, item));
if (item.docs != null) {
buf.writeln(
convertMarkdown(
item.docs!,
linkResolver: item.markdownLinkResolver(api.resolver),
),
);
}
}
}
}
var outline = Outline();
var outlineRenderer = OutlineRenderer();
if (library.exports.isNotEmpty) {
outline.add(Heading('Exports', id: '_Exports', level: 2));
}
for (var group in library.groups.values) {
outline.add(Heading(group.name, id: group.anchorId, level: 2));
if (!group.containerType) {
for (var item in group.items) {
outline.add(
Heading(
outlineRenderer.render(group.type, item),
id: item.anchorId,
level: 3,
),
);
}
}
}
return GenerationResults(buf.toString(), outline);
}
}
class _InterfaceElementGenerator {
final InterfaceElementItems classItems;
final Workspace workspace;
final WorkspaceFile thisFile;
_InterfaceElementGenerator({
required this.classItems,
required this.workspace,
required this.thisFile,
});
GenerationResults generate() {
var api = workspace.api;
var linkedCodeRenderer = LinkedCodeRenderer(api.resolver, thisFile);
var buf = StringBuffer();
buf.writeln('<h1>${classItems.name}</h1>');
buf.write(writeAnnotations(classItems));
buf.writeln(linkedCodeRenderer.render(classItems.type, classItems));
writeAncestors(buf);
_writeChildRelationships(
buf,
classItems.relationships,
thisFile,
workspace.api,
);
if (classItems.docs != null) {
buf.writeln(
convertMarkdown(
classItems.docs!,
linkResolver: classItems.markdownLinkResolver(api.resolver),
),
);
}
var pageItemRenderer = OutlineRenderer();
for (var group in classItems.groups.values) {
buf.writeln('<h2 id="${group.anchorId}">${group.name}</h2>');
if (group.type == GroupType.enumValue) {
// For enums, add a 'values' table.
buf.writeln('<table>');
// todo: also include information about the enum's value
for (var item in group.items) {
buf.write('<tr>');
buf.writeln('<td id="${item.anchorId}">${item.name}</td>');
buf.write('<td class="item-docs">');
if (item.docs != null) {
buf.writeln(
convertMarkdown(
item.docs!,
linkResolver: item.markdownLinkResolver(api.resolver),
),
);
}
buf.write('</td></tr>');
}
buf.writeln('</table>');
} else {
for (var item in group.items) {
buf.writeln(
'<h3 id="${item.anchorId}">${pageItemRenderer.render(group.type, item)}'
'<span class="symbol-type">${item.element.kind.displayName}</span>'
'</h3>',
);
buf.write(writeAnnotations(item));
buf.writeln(linkedCodeRenderer.render(group.type, item));
if (item.docs != null) {
buf.writeln(
convertMarkdown(
item.docs!,
linkResolver: item.markdownLinkResolver(api.resolver),
),
);
}
}
}
}
var outline = Outline();
var outlineRenderer = OutlineRenderer();
for (var group in classItems.groups.values) {
outline.add(Heading(group.name, id: group.anchorId, level: 2));
if (group.type != GroupType.enumValue) {
for (var item in group.items) {
outline.add(
Heading(
outlineRenderer.render(group.type, item),
id: item.anchorId,
level: 3,
),
);
}
}
}
return GenerationResults(buf.toString(), outline);
}
void writeAncestors(StringBuffer out) {
var buf = StringBuffer();
final api = workspace.api;
var element = classItems.element;
var superElement = element.supertype?.element;
if (superElement != null) {
buf.writeln('<tr><td class="item-title">Extends</td>');
buf.write('<td class="item-docs">');
var ref = api.hrefOrSpan(superElement.name, superElement, from: thisFile);
buf.write('<code>$ref</code></td></tr>');
}
if (element.interfaces.isNotEmpty) {
buf.writeln('<tr><td class="item-title">Implements</td>');
buf.write('<td class="item-docs">');
buf.write(
element.interfaces
.map(
(i) => api.hrefOrSpan(i.element.name, i.element, from: thisFile),
)
.map((s) => '<code>$s</code>')
.join(', '),
);
buf.write('</td></tr>');
}
if (element.mixins.isNotEmpty) {
buf.writeln('<tr><td class="item-title">Mixins</td>');
buf.write('<td class="item-docs">');
buf.write(
element.mixins
.map(
(m) => api.hrefOrSpan(m.element.name, m.element, from: thisFile),
)
.map((s) => '<code>$s</code>')
.join(', '),
);
buf.write('</td></tr>');
}
if (buf.isNotEmpty) {
out.writeln('<table>');
out.write(buf.toString());
out.writeln('</table>');
}
}
}
class _ExtentionElementGenerator {
final ExtensionElementItems extensionItems;
final Workspace workspace;
final WorkspaceFile thisFile;
_ExtentionElementGenerator({
required this.extensionItems,
required this.workspace,
required this.thisFile,
});
GenerationResults generate() {
var api = workspace.api;
var linkedCodeRenderer = LinkedCodeRenderer(api.resolver, thisFile);
var buf = StringBuffer();
buf.writeln('<h1>${extensionItems.name}</h1>');
buf.write(writeAnnotations(extensionItems));
buf.writeln(linkedCodeRenderer.render(extensionItems.type, extensionItems));
writeAncestors(buf);
_writeChildRelationships(
buf,
extensionItems.relationships,
thisFile,
workspace.api,
);
if (extensionItems.docs != null) {
buf.writeln(
convertMarkdown(
extensionItems.docs!,
linkResolver: extensionItems.markdownLinkResolver(api.resolver),
),
);
}
var pageItemRenderer = OutlineRenderer();
for (var group in extensionItems.groups.values) {
buf.writeln('<h2 id="${group.anchorId}">${group.name}</h2>');
for (var item in group.items) {
buf.writeln(
'<h3 id="${item.anchorId}">${pageItemRenderer.render(group.type, item)}'
'<span class="symbol-type">${item.element.kind.displayName}</span>'
'</h3>',
);
buf.write(writeAnnotations(item));
buf.writeln(linkedCodeRenderer.render(group.type, item));
if (item.docs != null) {
buf.writeln(
convertMarkdown(
item.docs!,
linkResolver: item.markdownLinkResolver(api.resolver),
),
);
}
}
}
var outline = Outline();
var outlineRenderer = OutlineRenderer();
for (var group in extensionItems.groups.values) {
outline.add(Heading(group.name, id: group.anchorId, level: 2));
for (var item in group.items) {
outline.add(
Heading(
outlineRenderer.render(group.type, item),
id: item.anchorId,
level: 3,
),
);
}
}
return GenerationResults(buf.toString(), outline);
}
void writeAncestors(StringBuffer out) {
var buf = StringBuffer();
final api = workspace.api;
var element = extensionItems.asExtension;
var extendedElement = element.extendedType.element;
if (extendedElement != null && extendedElement.name != null) {
buf.writeln('<tr><td class="item-title">Extension on</td>');
buf.write('<td class="item-docs">');
var ref = api.hrefOrSpan(
extendedElement.name!,
extendedElement,
from: thisFile,
);
buf.write('<code>$ref</code></td></tr>');
}
if (buf.isNotEmpty) {
out.writeln('<table>');
out.write(buf.toString());
out.writeln('</table>');
}
}
}
void _writeChildRelationships(
StringBuffer buf,
RelationshipMap relationships,
WorkspaceFile thisFile,
Api api,
) {
if (relationships.isEmpty) return;
buf.writeln('<table>');
for (var kind in relationships.keys) {
buf.write('<tr>');
buf.write('<td class="item-title">${kind.title}</td>');
buf.write('<td class="item-docs">');
var items = relationships[kind]!;
items.sort((a, b) => adjustedLexicalCompare(a.name, b.name));
buf.write(
items
.map((item) {
var ref = api.hrefOrSpan(item.name, item.element, from: thisFile);
return '<code>$ref</code>';
})
.join(', '),
);
buf.writeln('</td></tr>');
}
buf.writeln('</table>');
}