| // 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. |
| |
| import 'dart:convert'; |
| |
| import '../log/log.dart'; |
| import '../server.dart'; |
| |
| typedef Writer = void Function(StringSink sink); |
| |
| /// A class used to write an HTML page. |
| abstract class PageWriter { |
| /// The object used to escape special HTML characters. |
| static final HtmlEscape htmlEscape = HtmlEscape(); |
| |
| /// Initialize a newly create page writer. |
| PageWriter(); |
| |
| /// Return the length of the common prefix for time stamps associated with the |
| /// given log [entries]. |
| int computePrefixLength(List<LogEntry> entries) { |
| var length = entries.length; |
| if (length < 2) { |
| return 0; |
| } |
| var firstTime = entries[0].timeStamp.toString(); |
| var lastTime = entries[length - 1].timeStamp.toString(); |
| var prefixLength = 0; |
| var timeLength = firstTime.length; |
| while (prefixLength < timeLength && |
| firstTime.codeUnitAt(prefixLength) == |
| lastTime.codeUnitAt(prefixLength)) { |
| prefixLength++; |
| } |
| return prefixLength; |
| } |
| |
| /// Return an escaped version of the given [unsafe] text. |
| String escape(String unsafe) { |
| // We double escape single quotes because the escaped characters are |
| // processed as part of reading the HTML, which means that single quotes |
| // end up terminating string literals too early when they appear in event |
| // handlers (which in turn leads to JavaScript syntax errors). |
| return htmlEscape.convert(unsafe).replaceAll(''', '&#39;'); |
| } |
| |
| /// Write the body of the page (without the 'body' tag) to the given [sink]. |
| void writeBody(StringSink sink); |
| |
| /// Write the given [date] to the given [sink]. |
| void writeDate(StringSink sink, DateTime date) { |
| var isoString = date.toIso8601String(); |
| var index = isoString.indexOf('T'); |
| var dateString = isoString.substring(0, index); |
| var timeString = isoString.substring(index + 1); |
| sink.write(dateString); |
| sink.write(' at '); |
| sink.write(timeString); |
| } |
| |
| /// Write the body of the page (without the 'body' tag) to the given [sink]. |
| void writeMenu(StringSink sink) { |
| sink.writeln('<div class="menu">'); |
| sink.write('<a href="${WebServer.logPath}" class="menuItem">Log</a>'); |
| sink.write(' • '); |
| sink.write('<a href="${WebServer.statsPath}" class="menuItem">Stats</a>'); |
| sink.writeln('</div>'); |
| } |
| |
| /// Write the contents of the instrumentation log to the given [sink]. |
| void writePage(StringSink sink) { |
| sink.writeln('<!DOCTYPE html>'); |
| sink.writeln('<html lang="en-US">'); |
| sink.writeln('<head>'); |
| sink.writeln('<meta charset="utf-8">'); |
| sink.writeln( |
| '<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0">'); |
| sink.writeln('<title>Instrumentation Log</title>'); |
| sink.writeln('<style>'); |
| writeStyleSheet(sink); |
| sink.writeln('</style>'); |
| sink.writeln('<script>'); |
| writeScripts(sink); |
| sink.writeln('</script>'); |
| sink.writeln('</head>'); |
| sink.writeln('<body>'); |
| writeBody(sink); |
| sink.writeln('</body>'); |
| sink.writeln('</html>'); |
| } |
| |
| /// Write the scripts for the page (without the 'script' tag) to the given |
| /// [sink]. |
| void writeScripts(StringSink sink) { |
| // No common scripts. |
| } |
| |
| /// Write the content of the style sheet (without the 'script' tag) for the |
| /// page to the given [sink]. |
| void writeStyleSheet(StringSink sink) { |
| sink.writeln(r''' |
| a { |
| color: #000000; |
| text-decoration: none; |
| } |
| a.menuItem { |
| font-weight: bold; |
| } |
| body { |
| font-family: sans-serif; |
| height: 100%; |
| margin: 0px; |
| overflow: hidden; |
| padding: 0px; |
| width: 100%; |
| } |
| div.columnHeader { |
| } |
| div.button { |
| display: inline-block; |
| border-radius: 4px; |
| border: 1px solid; |
| height: 16px; |
| text-align: center; |
| vertical-align: middle; |
| width: 16px; |
| } |
| div.inset { |
| padding: 10px; |
| } |
| div.menu { |
| background-color: #cce6ff; |
| padding: 5px; |
| } |
| html { |
| height: 100%; |
| width: 100%; |
| } |
| span.button { |
| border-radius: 5px; |
| border: 1px solid; |
| height: 16px; |
| width: 16px; |
| } |
| span.error { |
| color: #ff0000; |
| } |
| span.gray { |
| color: #777777; |
| } |
| span.label { |
| font-weight: bold; |
| } |
| table.fullWidth { |
| border: 0px; |
| width: 100%; |
| } |
| td.halfWidth { |
| width: 50%; |
| vertical-align: top; |
| } |
| td.int { |
| text-align: right; |
| } |
| th { |
| text-align: left; |
| } |
| th.narrow { |
| width: 16px; |
| } |
| |
| #container { |
| height: 100%; |
| min-height: 100%; |
| position: relative; |
| width: 100%; |
| } |
| #content { |
| height: 90%; |
| width: 100%; |
| } |
| '''); |
| } |
| |
| /// Write to the given [sink] the HTML required to display content in two |
| /// columns. The content of the columns will be written by the functions |
| /// [writeLeftColumn], [writeCenterColumn] and [writeRightColumn] and will be |
| /// contained in 'div' elements with the id's [leftColumnId], [centerColumnId] |
| /// and [rightColumnId]. |
| void writeThreeColumns( |
| StringSink sink, |
| String leftColumnId, |
| Writer writeLeftColumn, |
| String centerColumnId, |
| Writer writeCenterColumn, |
| String rightColumnId, |
| Writer writeRightColumn) { |
| sink.writeln('<div>'); |
| sink.writeln(' <div>'); |
| sink.writeln(' <div id="$leftColumnId">'); |
| sink.writeln(' <div class="inset">'); |
| writeLeftColumn(sink); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln(' <div id="$rightColumnId">'); |
| sink.writeln(' <div class="inset">'); |
| writeRightColumn(sink); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln(' <div id="$centerColumnId">'); |
| sink.writeln(' <div class="inset">'); |
| writeCenterColumn(sink); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln('</div>'); |
| } |
| |
| /// Writeto the given [sink] the styles needed by a three column section where |
| /// the columns have the ids [leftColumnId], [centerColumnId] and |
| /// [rightColumnId]. |
| void writeThreeColumnStyles(StringSink sink, String leftColumnId, |
| String centerColumnId, String rightColumnId) { |
| sink.writeln(''' |
| #$leftColumnId { |
| float: left; |
| height: 100%; |
| overflow: auto; |
| width: 33%; |
| } |
| #$centerColumnId { |
| height: 100%; |
| overflow: auto; |
| width: 33%; |
| } |
| #$rightColumnId { |
| float: right; |
| height: 100%; |
| overflow: auto; |
| width: 33%; |
| } |
| '''); |
| } |
| |
| /// Write to the given [sink] the HTML required to display content in two |
| /// columns. The content of the columns will be written by the functions |
| /// [writeLeftColumn] and [writeRightColumn] and will be contained in 'div' |
| /// elements with the id's [leftColumnId] and [rightColumnId]. |
| void writeTwoColumns(StringSink sink, String leftColumnId, |
| Writer writeLeftColumn, String rightColumnId, Writer writeRightColumn) { |
| sink.writeln('<div id="container">'); |
| sink.writeln(' <div id="content">'); |
| sink.writeln(' <div id="$leftColumnId">'); |
| sink.writeln(' <div class="inset">'); |
| writeLeftColumn(sink); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln(' <div id="$rightColumnId">'); |
| sink.writeln(' <div class="inset">'); |
| writeRightColumn(sink); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln(' </div>'); |
| sink.writeln('</div>'); |
| } |
| |
| /// Writeto the given [sink] the styles needed by a two column section where |
| /// the columns have the ids [leftColumnId] and [rightColumnId]. |
| void writeTwoColumnStyles( |
| StringSink sink, String leftColumnId, String rightColumnId) { |
| sink.writeln(''' |
| #$leftColumnId { |
| float: left; |
| height: 100%; |
| overflow: auto; |
| width: 50%; |
| } |
| #$rightColumnId { |
| float: right; |
| height: 100%; |
| overflow: auto; |
| width: 50%; |
| } |
| '''); |
| } |
| } |