blob: b09db4848835482639596f08d3b0107829642546 [file] [log] [blame]
// Copyright (c) 2014, 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.
library dartdoc.html_generator;
import 'dart:async' show Future, StreamController, Stream;
import 'dart:io' show Directory, File;
import 'dart:isolate';
import 'package:dartdoc/dartdoc.dart';
import 'package:dartdoc/src/empty_generator.dart';
import 'package:dartdoc/src/generator.dart';
import 'package:dartdoc/src/html/html_generator_instance.dart';
import 'package:dartdoc/src/html/template_data.dart';
import 'package:dartdoc/src/html/templates.dart';
import 'package:dartdoc/src/model.dart';
import 'package:path/path.dart' as pathLib;
typedef String Renderer(String input);
// Generation order for libraries:
// constants
// typedefs
// properties
// functions
// enums
// classes
// exceptions
//
// Generation order for classes:
// constants
// static properties
// static methods
// properties
// constructors
// operators
// methods
class HtmlGenerator extends Generator {
final Templates _templates;
final HtmlGeneratorOptions _options;
HtmlGeneratorInstance _instance;
final StreamController<void> _onFileCreated =
new StreamController(sync: true);
@override
Stream<void> get onFileCreated => _onFileCreated.stream;
@override
final Set<String> writtenFiles = new Set<String>();
static Future<HtmlGenerator> create(
{HtmlGeneratorOptions options,
List<String> headers,
List<String> footers,
List<String> footerTexts}) async {
var templates = await Templates.create(
headerPaths: headers,
footerPaths: footers,
footerTextPaths: footerTexts);
return new HtmlGenerator._(
options ?? new HtmlGeneratorOptions(), templates);
}
HtmlGenerator._(this._options, this._templates);
@override
/// Actually write out the documentation for [packageGraph].
/// Stores the HtmlGeneratorInstance so we can access it in [writtenFiles].
Future generate(PackageGraph packageGraph, String outputDirectoryPath) async {
assert(_instance == null);
var enabled = true;
void write(String filePath, Object content, {bool allowOverwrite}) {
allowOverwrite ??= false;
if (!enabled) {
throw new StateError('`write` was called after `generate` completed.');
}
// If you see this assert, we're probably being called to build non-canonical
// docs somehow. Check data.self.isCanonical and callers for bugs.
assert(allowOverwrite || !writtenFiles.contains(filePath));
var file = new File(pathLib.join(outputDirectoryPath, filePath));
var parent = file.parent;
if (!parent.existsSync()) {
parent.createSync(recursive: true);
}
if (content is String) {
file.writeAsStringSync(content);
} else if (content is List<int>) {
file.writeAsBytesSync(content);
} else {
throw new ArgumentError.value(
content, 'content', '`content` must be `String` or `List<int>`.');
}
_onFileCreated.add(file);
writtenFiles.add(filePath);
}
try {
_instance =
new HtmlGeneratorInstance(_options, _templates, packageGraph, write);
await _instance.generate();
} finally {
enabled = false;
}
}
}
class HtmlGeneratorOptions implements HtmlOptions {
final String url;
final String faviconPath;
final bool prettyIndexJson;
@override
final String relCanonicalPrefix;
@override
final String toolVersion;
HtmlGeneratorOptions(
{this.url,
this.relCanonicalPrefix,
this.faviconPath,
String toolVersion,
this.prettyIndexJson = false})
: this.toolVersion = toolVersion ?? 'unknown';
}
Future<List<Generator>> initEmptyGenerators(DartdocOptionContext config) async {
return [EmptyGenerator()];
}
/// Initialize and setup the generators.
Future<List<Generator>> initGenerators(GeneratorContext config) async {
// TODO(jcollins-g): Rationalize based on GeneratorContext all the way down
// through the generators.
HtmlGeneratorOptions options = new HtmlGeneratorOptions(
url: config.hostedUrl,
relCanonicalPrefix: config.relCanonicalPrefix,
toolVersion: dartdocVersion,
faviconPath: config.favicon,
prettyIndexJson: config.prettyIndexJson);
return [
await HtmlGenerator.create(
options: options,
headers: config.header,
footers: config.footer,
footerTexts: config.footerTextPaths,
)
];
}
Uri _sdkFooterCopyrightUri;
Future<void> _setSdkFooterCopyrightUri() async {
if (_sdkFooterCopyrightUri == null) {
_sdkFooterCopyrightUri = await Isolate.resolvePackageUri(
Uri.parse('package:dartdoc/resources/sdk_footer_text.html'));
}
}
abstract class GeneratorContext implements DartdocOptionContext {
String get favicon => optionSet['favicon'].valueAt(context);
List<String> get footer => optionSet['footer'].valueAt(context);
/// _footerText is only used to construct synthetic options.
// ignore: unused_element
List<String> get _footerText => optionSet['footerText'].valueAt(context);
List<String> get footerTextPaths =>
optionSet['footerTextPaths'].valueAt(context);
List<String> get header => optionSet['header'].valueAt(context);
String get hostedUrl => optionSet['hostedUrl'].valueAt(context);
bool get prettyIndexJson => optionSet['prettyIndexJson'].valueAt(context);
String get relCanonicalPrefix =>
optionSet['relCanonicalPrefix'].valueAt(context);
}
Future<List<DartdocOption>> createGeneratorOptions() async {
await _setSdkFooterCopyrightUri();
return <DartdocOption>[
new DartdocOptionArgFile<String>('favicon', null,
isFile: true,
help: 'A path to a favicon for the generated docs.',
mustExist: true),
new DartdocOptionArgFile<List<String>>('footer', [],
isFile: true,
help: 'paths to footer files containing HTML text.',
mustExist: true,
splitCommas: true),
new DartdocOptionArgFile<List<String>>('footerText', [],
isFile: true,
help:
'paths to footer-text files (optional text next to the package name '
'and version).',
mustExist: true,
splitCommas: true),
new DartdocOptionSyntheticOnly<List<String>>(
'footerTextPaths',
(DartdocSyntheticOption<List<String>> option, Directory dir) {
final List<String> footerTextPaths = <String>[];
final PackageMeta topLevelPackageMeta =
option.root['topLevelPackageMeta'].valueAt(dir);
// TODO(jcollins-g): Eliminate special casing for SDK and use config file.
if (topLevelPackageMeta.isSdk == true) {
footerTextPaths
.add(pathLib.canonicalize(_sdkFooterCopyrightUri.toFilePath()));
}
footerTextPaths.addAll(option.parent['footerText'].valueAt(dir));
return footerTextPaths;
},
isFile: true,
help: 'paths to footer-text-files (adding special case for SDK)',
mustExist: true,
),
new DartdocOptionArgFile<List<String>>('header', [],
isFile: true,
help: 'paths to header files containing HTML text.',
splitCommas: true),
new DartdocOptionArgOnly<String>('hostedUrl', null,
help:
'URL where the docs will be hosted (used to generate the sitemap).'),
new DartdocOptionArgOnly<bool>('prettyIndexJson', false,
help:
"Generates `index.json` with indentation and newlines. The file is larger, but it's also easier to diff.",
negatable: false),
new DartdocOptionArgOnly<String>('relCanonicalPrefix', null,
help:
'If provided, add a rel="canonical" prefixed with provided value. '
'Consider using if\nbuilding many versions of the docs for public '
'SEO; learn more at https://goo.gl/gktN6F.'),
];
}