blob: eae6992144bef6010105fca7307f2c9aad3fcbd7 [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.
// @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;
}