20faaa5d94
Change-Id: Ib18f554c1076f27a3f7c0df5a36410da41a1f467 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/148784 Reviewed-by: Paul Berry <paulberry@google.com> Reviewed-by: Sigmund Cherem <sigmund@google.com>
505 lines
14 KiB
Dart
505 lines
14 KiB
Dart
// 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.
|
|
|
|
// @dart = 2.7
|
|
|
|
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,
|
|
{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 <String, String>{}});
|
|
|
|
@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(),
|
|
ElementScheme elementScheme: const ElementScheme()}) {
|
|
return (Iterable<Annotation> annotations, {bool forSpan}) {
|
|
return getAnnotationDataFromSchemes(annotations,
|
|
forSpan: forSpan,
|
|
colorScheme: colorScheme,
|
|
elementScheme: elementScheme);
|
|
};
|
|
}
|
|
|
|
LineData getDefaultLineData(data) => const LineData();
|
|
|
|
AnnotationData getAnnotationDataFromSchemes(Iterable<Annotation> annotations,
|
|
{bool forSpan,
|
|
CssColorScheme colorScheme: const SingleColorScheme(),
|
|
ElementScheme elementScheme: const ElementScheme()}) {
|
|
if (colorScheme.showLocationAsSpan != forSpan) return null;
|
|
Map<String, String> data = <String, String>{};
|
|
var id;
|
|
if (annotations.length == 1) {
|
|
Annotation annotation = annotations.single;
|
|
if (annotation != null) {
|
|
id = annotation.id;
|
|
data['style'] = colorScheme.singleLocationToCssColor(id);
|
|
data['title'] = annotation.title;
|
|
}
|
|
} else {
|
|
id = annotations.first.id;
|
|
List 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 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 new 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 new 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 new 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);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 new 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 new 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 new 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 = new 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: [
|
|
new TagPart(annotationData.tag,
|
|
properties: annotationData.properties,
|
|
content: [new HtmlText(head)]),
|
|
new 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 new 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 new 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 = new StringBuffer();
|
|
final List<CodePart> codeParts = <CodePart>[];
|
|
final List<Annotation> annotations = <Annotation>[];
|
|
var lineAnnotation;
|
|
String _code;
|
|
|
|
CodeLine(this.lineNo, this.offset, {this.uri});
|
|
|
|
@override
|
|
HtmlPartKind get kind => HtmlPartKind.CODE;
|
|
|
|
String get code {
|
|
if (_code == null) {
|
|
_code = codeBuffer.toString();
|
|
}
|
|
return _code;
|
|
}
|
|
|
|
@override
|
|
void printHtmlOn(StringBuffer htmlBuffer, HtmlPrintContext context) {
|
|
if (context.usePre) {
|
|
LineData lineData = context.getLineData(lineAnnotation);
|
|
htmlBuffer.write('<p class="${lineData.lineClass}">');
|
|
}
|
|
new 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 = new 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 new Annotation(json['id'], json['codeOffset'], json['title'],
|
|
data: json['data']);
|
|
}
|
|
|
|
encodeLineAnnotation(lineAnnotation) => lineAnnotation;
|
|
|
|
decodeLineAnnotation(json) => json;
|
|
}
|