// 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 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 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(), ElementScheme elementScheme: const ElementScheme()}) { return (Iterable annotations, {bool forSpan}) { return getAnnotationDataFromSchemes(annotations, forSpan: forSpan, colorScheme: colorScheme, elementScheme: elementScheme); }; } LineData getDefaultLineData(data) => const LineData(); AnnotationData getAnnotationDataFromSchemes(Iterable annotations, {bool forSpan, CssColorScheme colorScheme: const SingleColorScheme(), ElementScheme elementScheme: const ElementScheme()}) { if (colorScheme.showLocationAsSpan != forSpan) return null; Map data = {}; 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((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('
'); } } @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 properties; final List content; TagPart(this.tag, {this.properties: const {}, this.content: const []}); @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(''); } @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 htmlParts = []; @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 annotations; final String subsequentCode; CodePart(this.annotations, this.subsequentCode); void printHtmlOn(StringBuffer buffer, HtmlPrintContext context) { Iterable included = annotations.where(context.includeAnnotation); List htmlParts = []; 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 codeParts = []; final List annotations = []; 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('

'); } 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('

'); } } @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; }