| // 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 void Writer(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 = new 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) { |
| int length = entries.length; |
| if (length < 2) { |
| return 0; |
| } |
| String firstTime = entries[0].timeStamp.toString(); |
| String lastTime = entries[length - 1].timeStamp.toString(); |
| int prefixLength = 0; |
| int 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) { |
| String isoString = date.toIso8601String(); |
| int index = isoString.indexOf('T'); |
| String dateString = isoString.substring(0, index); |
| String 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%; |
| } |
| '''); |
| } |
| } |