blob: 3639323b9314cfa85a06bb68185b8e0aa4a3f61b [file] [log] [blame]
// Copyright (c) 2016, 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 sourcemap.html_parts;
import 'sourcemap_html_helper.dart';
class Annotation {
final id;
final int codeOffset;
final String title;
final data;
Annotation(this.id, this.codeOffset, this.title, {this.data});
}
typedef bool AnnotationFilter(Annotation annotation);
typedef AnnotationData? AnnotationDataFunction(
Iterable<Annotation> annotations, {
required bool forSpan,
});
typedef LineData LineDataFunction(lineAnnotation);
bool includeAllAnnotation(Annotation annotation) => true;
class LineData {
final String lineClass;
final String lineNumberClass;
const LineData({
this.lineClass = 'line',
this.lineNumberClass = 'lineNumber',
});
}
class AnnotationData {
final String tag;
final Map<String, String?> properties;
const AnnotationData({this.tag = 'a', this.properties = const {}});
@override
int get hashCode => tag.hashCode * 13 + properties.hashCode * 19;
@override
bool operator ==(other) {
if (identical(this, other)) return true;
if (other is! AnnotationData) return false;
return tag == other.tag &&
properties.length == other.properties.length &&
properties.keys.every((k) => properties[k] == other.properties[k]);
}
}
AnnotationDataFunction createAnnotationDataFunction({
CssColorScheme colorScheme = const SingleColorScheme(),
required ElementScheme elementScheme,
}) {
return (Iterable<Annotation> annotations, {required bool forSpan}) {
return getAnnotationDataFromSchemes(
annotations,
forSpan: forSpan,
colorScheme: colorScheme,
elementScheme: elementScheme,
);
};
}
LineData getDefaultLineData(data) => const LineData();
AnnotationData? getAnnotationDataFromSchemes(
Iterable<Annotation> annotations, {
required bool forSpan,
CssColorScheme colorScheme = const SingleColorScheme(),
ElementScheme elementScheme = const ElementScheme(),
}) {
if (colorScheme.showLocationAsSpan != forSpan) return null;
Map<String, String?> data = {};
var id;
if (annotations.length == 1) {
Annotation annotation = annotations.single;
id = annotation.id;
data['style'] = colorScheme.singleLocationToCssColor(id);
data['title'] = annotation.title;
} else {
id = annotations.first.id;
List<int> ids = [];
for (Annotation annotation in annotations) {
ids.add(annotation.id);
}
data['style'] = colorScheme.multiLocationToCssColor(ids);
data['title'] = annotations.map((l) => l.title).join(',');
}
if (id != null) {
Set<int> ids = annotations.map<int>((l) => l.id).toSet();
data['name'] = elementScheme.getName(id, ids);
data['href'] = elementScheme.getHref(id, ids);
data['onclick'] = elementScheme.onClick(id, ids);
data['onmouseover'] = elementScheme.onMouseOver(id, ids);
data['onmouseout'] = elementScheme.onMouseOut(id, ids);
return AnnotationData(properties: data);
}
return null;
}
class HtmlPrintContext {
final int? lineNoWidth;
final bool usePre;
final AnnotationFilter includeAnnotation;
final AnnotationDataFunction getAnnotationData;
final LineDataFunction getLineData;
HtmlPrintContext({
this.lineNoWidth,
this.usePre = true,
this.includeAnnotation = includeAllAnnotation,
this.getAnnotationData = getAnnotationDataFromSchemes,
this.getLineData = getDefaultLineData,
});
HtmlPrintContext from({
int? lineNoWidth,
bool? usePre,
AnnotationFilter? includeAnnotation,
AnnotationDataFunction? getAnnotationData,
LineDataFunction? getLineData,
}) {
return HtmlPrintContext(
lineNoWidth: lineNoWidth ?? this.lineNoWidth,
usePre: usePre ?? this.usePre,
includeAnnotation: includeAnnotation ?? this.includeAnnotation,
getAnnotationData: getAnnotationData ?? this.getAnnotationData,
getLineData: getLineData ?? this.getLineData,
);
}
}
enum HtmlPartKind { CODE, LINE, CONST, NEWLINE, TEXT, TAG, LINE_NUMBER }
abstract class HtmlPart {
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context);
HtmlPartKind get kind;
toJson(JsonStrategy strategy);
static HtmlPart fromJson(json, JsonStrategy strategy) {
if (json is String) {
return ConstHtmlPart(json);
} else {
switch (HtmlPartKind.values[json['kind']]) {
case HtmlPartKind.LINE:
return HtmlLine.fromJson(json, strategy);
case HtmlPartKind.CODE:
return CodeLine.fromJson(json, strategy);
case HtmlPartKind.CONST:
return ConstHtmlPart.fromJson(json, strategy);
case HtmlPartKind.NEWLINE:
return const NewLine();
case HtmlPartKind.TEXT:
return HtmlText.fromJson(json, strategy);
case HtmlPartKind.TAG:
return TagPart.fromJson(json, strategy);
case HtmlPartKind.LINE_NUMBER:
return LineNumber.fromJson(json, strategy);
}
}
}
}
class ConstHtmlPart implements HtmlPart {
final String html;
const ConstHtmlPart(this.html);
@override
HtmlPartKind get kind => HtmlPartKind.CONST;
@override
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
buffer.write(html);
}
@override
toJson(JsonStrategy strategy) {
return {'kind': kind.index, 'html': html};
}
static ConstHtmlPart fromJson(Map json, JsonStrategy strategy) {
return ConstHtmlPart(json['html']);
}
}
class NewLine implements HtmlPart {
const NewLine();
@override
HtmlPartKind get kind => HtmlPartKind.NEWLINE;
@override
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
if (context.usePre) {
buffer.write('\n');
} else {
buffer.write('<br/>');
}
}
@override
toJson(JsonStrategy strategy) {
return {'kind': kind.index};
}
}
class HtmlText implements HtmlPart {
final String text;
const HtmlText(this.text);
@override
HtmlPartKind get kind => HtmlPartKind.TEXT;
@override
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
String escaped = escape(text);
buffer.write(escaped);
}
@override
toJson(JsonStrategy strategy) {
return {'kind': kind.index, 'text': text};
}
static HtmlText fromJson(Map json, JsonStrategy strategy) {
return HtmlText(json['text']);
}
}
class TagPart implements HtmlPart {
final String tag;
final Map<String, String?> properties;
final List<HtmlPart> content;
TagPart(
this.tag, {
this.properties = const <String, String>{},
this.content = const <HtmlPart>[],
});
@override
HtmlPartKind get kind => HtmlPartKind.TAG;
@override
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
buffer.write('<$tag');
properties.forEach((String key, String? value) {
if (value != null) {
buffer.write(' $key="${value}"');
}
});
buffer.write('>');
for (HtmlPart child in content) {
child.printHtmlOn(buffer, context);
}
buffer.write('</$tag>');
}
@override
toJson(JsonStrategy strategy) {
return {
'kind': kind.index,
'tag': tag,
'properties': properties,
'content': content.map((p) => p.toJson(strategy)).toList(),
};
}
static TagPart fromJson(Map json, JsonStrategy strategy) {
return TagPart(
json['tag'],
properties: json['properties'],
content: json['content'].map(HtmlPart.fromJson).toList(),
);
}
}
class HtmlLine implements HtmlPart {
final List<HtmlPart> htmlParts = <HtmlPart>[];
@override
HtmlPartKind get kind => HtmlPartKind.LINE;
@override
void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
for (HtmlPart part in htmlParts) {
part.printHtmlOn(htmlBuffer, context);
}
}
@override
Map toJson(JsonStrategy strategy) {
return {
'kind': kind.index,
'html': htmlParts.map((p) => p.toJson(strategy)).toList(),
};
}
static HtmlLine fromJson(Map json, JsonStrategy strategy) {
HtmlLine line = HtmlLine();
json['html'].forEach(
(part) => line.htmlParts.add(HtmlPart.fromJson(part, strategy)),
);
return line;
}
}
class CodePart {
final List<Annotation> annotations;
final String subsequentCode;
CodePart(this.annotations, this.subsequentCode);
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
Iterable<Annotation> included = annotations.where(
context.includeAnnotation,
);
List<HtmlPart> htmlParts = <HtmlPart>[];
if (included.isNotEmpty) {
AnnotationData? annotationData = context.getAnnotationData(
included,
forSpan: false,
);
AnnotationData? annotationDataForSpan = context.getAnnotationData(
included,
forSpan: true,
);
String head = subsequentCode;
String tail = '';
if (subsequentCode.length > 1) {
head = subsequentCode.substring(0, 1);
tail = subsequentCode.substring(1);
}
if (annotationData != null && annotationDataForSpan != null) {
htmlParts.add(
new TagPart(
annotationDataForSpan.tag,
properties: annotationDataForSpan.properties,
content: [
TagPart(
annotationData.tag,
properties: annotationData.properties,
content: [new HtmlText(head)],
),
HtmlText(tail),
],
),
);
} else if (annotationDataForSpan != null) {
htmlParts.add(
new TagPart(
annotationDataForSpan.tag,
properties: annotationDataForSpan.properties,
content: [new HtmlText(subsequentCode)],
),
);
} else if (annotationData != null) {
htmlParts.add(
new TagPart(
annotationData.tag,
properties: annotationData.properties,
content: [new HtmlText(head)],
),
);
htmlParts.add(new HtmlText(tail));
} else {
htmlParts.add(new HtmlText(subsequentCode));
}
} else {
htmlParts.add(new HtmlText(subsequentCode));
}
for (HtmlPart part in htmlParts) {
part.printHtmlOn(buffer, context);
}
}
Map toJson(JsonStrategy strategy) {
return {
'annotations': annotations
.map((a) => strategy.encodeAnnotation(a))
.toList(),
'subsequentCode': subsequentCode,
};
}
static CodePart fromJson(Map json, JsonStrategy strategy) {
return CodePart(
json['annotations'].map((j) => strategy.decodeAnnotation(j)).toList(),
json['subsequentCode'],
);
}
}
class LineNumber extends HtmlPart {
final int lineNo;
final lineAnnotation;
LineNumber(this.lineNo, this.lineAnnotation);
@override
HtmlPartKind get kind => HtmlPartKind.LINE_NUMBER;
@override
toJson(JsonStrategy strategy) {
return {
'kind': kind.index,
'lineNo': lineNo,
'lineAnnotation': strategy.encodeLineAnnotation(lineAnnotation),
};
}
static LineNumber fromJson(Map json, JsonStrategy strategy) {
return LineNumber(
json['lineNo'],
strategy.decodeLineAnnotation(json['lineAnnotation']),
);
}
@override
void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) {
buffer.write(
lineNumber(
lineNo,
width: context.lineNoWidth,
useNbsp: !context.usePre,
className: context.getLineData(lineAnnotation).lineNumberClass,
),
);
}
}
class CodeLine extends HtmlPart {
final Uri? uri;
final int lineNo;
final int offset;
final StringBuffer codeBuffer = StringBuffer();
final List<CodePart> codeParts = <CodePart>[];
final List<Annotation> annotations = <Annotation>[];
var lineAnnotation;
CodeLine(this.lineNo, this.offset, {this.uri});
@override
HtmlPartKind get kind => HtmlPartKind.CODE;
late final String code = codeBuffer.toString();
@override
void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
if (context.usePre) {
LineData lineData = context.getLineData(lineAnnotation);
htmlBuffer.write('<p class="${lineData.lineClass}">');
}
LineNumber(lineNo, lineAnnotation).printHtmlOn(htmlBuffer, context);
for (CodePart part in codeParts) {
part.printHtmlOn(htmlBuffer, context);
}
const NewLine().printHtmlOn(htmlBuffer, context);
if (context.usePre) {
htmlBuffer.write('</p>');
}
}
@override
Map toJson(JsonStrategy strategy) {
return {
'kind': kind.index,
'lineNo': lineNo,
'offset': offset,
'code': code,
'parts': codeParts.map((p) => p.toJson(strategy)).toList(),
'annotations': annotations
.map((a) => strategy.encodeAnnotation(a))
.toList(),
'lineAnnotation': lineAnnotation != null
? strategy.encodeLineAnnotation(lineAnnotation)
: null,
};
}
static CodeLine fromJson(Map json, JsonStrategy strategy) {
CodeLine line = CodeLine(
json['lineNo'],
json['offset'],
uri: json['uri'] != null ? Uri.parse(json['uri']) : null,
);
line.codeBuffer.write(json['code']);
json['parts'].forEach(
(part) => line.codeParts.add(CodePart.fromJson(part, strategy)),
);
json['annotations'].forEach(
(a) => line.annotations.add(strategy.decodeAnnotation(a)),
);
line.lineAnnotation = json['lineAnnotation'] != null
? strategy.decodeLineAnnotation(json['lineAnnotation'])
: null;
return line;
}
}
class JsonStrategy {
const JsonStrategy();
Map encodeAnnotation(Annotation annotation) {
return {
'id': annotation.id,
'codeOffset': annotation.codeOffset,
'title': annotation.title,
'data': annotation.data,
};
}
Annotation decodeAnnotation(Map json) {
return Annotation(
json['id'],
json['codeOffset'],
json['title'],
data: json['data'],
);
}
encodeLineAnnotation(lineAnnotation) => lineAnnotation;
decodeLineAnnotation(json) => json;
}