Merge branch 'optimize-regexps' of https://github.com/lrhn/dart-markdown into regexp
diff --git a/pkgs/markdown/.gitignore b/pkgs/markdown/.gitignore index aeb4e13..492ac23 100644 --- a/pkgs/markdown/.gitignore +++ b/pkgs/markdown/.gitignore
@@ -1,5 +1,4 @@ +.idea +.packages +.pub packages -pubspec.lock -.project -.children -out \ No newline at end of file
diff --git a/pkgs/markdown/CHANGELOG.md b/pkgs/markdown/CHANGELOG.md index ffc8430..fdbb0d6 100644 --- a/pkgs/markdown/CHANGELOG.md +++ b/pkgs/markdown/CHANGELOG.md
@@ -1,4 +1,20 @@ -## v0.7.1+2 +## 0.8.0 + +* Switch tests to use [test][] instead of [unittest][]. +* Remove (probably unused) `resolved` field from `LinkSyntax`. + +[test]: https://pub.dartlang.org/packages/test +[unittest]: https://pub.dartlang.org/packages/unittest + +## 0.7.2 + +* Allow resolving links that contain inline syntax (#42). + +## 0.7.1+3 + +* Updated homepage. + +## 0.7.1+2 * Formatted code.
diff --git a/pkgs/markdown/benchmark/benchmark.dart b/pkgs/markdown/benchmark/benchmark.dart new file mode 100644 index 0000000..124a4f7 --- /dev/null +++ b/pkgs/markdown/benchmark/benchmark.dart
@@ -0,0 +1,73 @@ +// Copyright (c) 2015, 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. + +library markdown.benchmark.benchmark; + +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import 'package:markdown/markdown.dart'; + +const numTrials = 100; +const runsPerTrial = 50; + +final source = loadFile("input.md"); +final expected = loadFile("output.html"); + +void main(List<String> args) { + var best = 99999999.0; + + // Run the benchmark several times. This ensures the VM is warmed up and lets + // us see how much variance there is. + for (var i = 0; i <= numTrials; i++) { + var start = new DateTime.now(); + + // For a single benchmark, convert the source multiple times. + var result; + for (var j = 0; j < runsPerTrial; j++) { + result = markdownToHtml(source); + } + + var elapsed = + new DateTime.now().difference(start).inMilliseconds / runsPerTrial; + + // Keep track of the best run so far. + if (elapsed >= best) continue; + best = elapsed; + + // Sanity check to make sure the output is what we expect and to make sure + // the VM doesn't optimize "dead" code away. + if (result != expected) { + print("Incorrect output:\n$result"); + exit(1); + } + + // Don't print the first run. It's always terrible since the VM hasn't + // warmed up yet. + if (i == 0) continue; + printResult("Run ${padLeft('#$i', 3)}", elapsed); + } + + printResult("Best ", best); +} + +String loadFile(String name) { + var path = p.join(p.dirname(p.fromUri(Platform.script)), name); + return new File(path).readAsStringSync(); +} + +void printResult(String label, double time) { + print("$label: ${padLeft(time.toStringAsFixed(2), 4)}ms " + "${'=' * ((time * 20).toInt())}"); +} + +String padLeft(input, int length) { + var result = input.toString(); + if (result.length < length) { + result = " " * (length - result.length) + result; + } + + return result; +}
diff --git a/pkgs/markdown/benchmark/input.md b/pkgs/markdown/benchmark/input.md new file mode 100644 index 0000000..31f5f57 --- /dev/null +++ b/pkgs/markdown/benchmark/input.md
@@ -0,0 +1,496 @@ +**TODO: Add more examples to cover all of the syntax.** + +This input was taken from the test package's README to get a representative +sample of real-world markdown: + +## Writing Tests + +Tests are specified using the top-level [`test()`][test] function, and test +assertions are made using [`expect()`][expect]: + +[test]: http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_test +[expect]: http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_expect + +```dart +import "package:test/test.dart"; + +void main() { + test("String.split() splits the string on the delimiter", () { + var string = "foo,bar,baz"; + expect(string.split(","), equals(["foo", "bar", "baz"])); + }); + + test("String.trim() removes surrounding whitespace", () { + var string = " foo "; + expect(string.trim(), equals("foo")); + }); +} +``` + +Tests can be grouped together using the [`group()`] function. Each group's +description is added to the beginning of its test's descriptions. + +```dart +import "package:test/test.dart"; + +void main() { + group("String", () { + test(".split() splits the string on the delimiter", () { + var string = "foo,bar,baz"; + expect(string.split(","), equals(["foo", "bar", "baz"])); + }); + + test(".trim() removes surrounding whitespace", () { + var string = " foo "; + expect(string.trim(), equals("foo")); + }); + }); + + group("int", () { + test(".remainder() returns the remainder of division", () { + expect(11.remainder(3), equals(2)); + }); + + test(".toRadixString() returns a hex string", () { + expect(11.toRadixString(16), equals("b")); + }); + }); +} +``` + +Any matchers from the [`matcher`][matcher] package can be used with `expect()` +to do complex validations: + +[matcher]: http://www.dartdocs.org/documentation/matcher/latest/index.html#matcher/matcher + +```dart +import "package:test/test.dart"; + +void main() { + test(".split() splits the string on the delimiter", () { + expect("foo,bar,baz", allOf([ + contains("foo"), + isNot(startsWith("bar")), + endsWith("baz") + ])); + }); +} +``` + +## Running Tests + +A single test file can be run just using `pub run test:test path/to/test.dart` +(on Dart 1.10, this can be shortened to `pub run test path/to/test.dart`). + + + +Many tests can be run at a time using `pub run test:test path/to/dir`. + + + +It's also possible to run a test on the Dart VM only by invoking it using `dart +path/to/test.dart`, but this doesn't load the full test runner and will be +missing some features. + +The test runner considers any file that ends with `_test.dart` to be a test +file. If you don't pass any paths, it will run all the test files in your +`test/` directory, making it easy to test your entire application at once. + +By default, tests are run in the Dart VM, but you can run them in the browser as +well by passing `pub run test:test -p chrome path/to/test.dart`. +`test` will take care of starting the browser and loading the tests, and all +the results will be reported on the command line just like for VM tests. In +fact, you can even run tests on both platforms with a single command: `pub run +test:test -p "chrome,vm" path/to/test.dart`. + +### Restricting Tests to Certain Platforms + +Some test files only make sense to run on particular platforms. They may use +`dart:html` or `dart:io`, they might test Windows' particular filesystem +behavior, or they might use a feature that's only available in Chrome. The +[`@TestOn`][TestOn] annotation makes it easy to declare exactly which platforms +a test file should run on. Just put it at the top of your file, before any +`library` or `import` declarations: + +```dart +@TestOn("vm") + +import "dart:io"; + +import "package:test/test.dart"; + +void main() { + // ... +} +``` + +[TestOn]: http://www.dartdocs.org/documentation/test/latest/index.html#test/test.TestOn + +The string you pass to `@TestOn` is what's called a "platform selector", and it +specifies exactly which platforms a test can run on. It can be as simple as the +name of a platform, or a more complex Dart-like boolean expression involving +these platform names. + +### Platform Selector Syntax + +Platform selectors can contain identifiers, parentheses, and operators. When +loading a test, each identifier is set to `true` or `false` based on the current +platform, and the test is only loaded if the platform selector returns `true`. +The operators `||`, `&&`, `!`, and `? :` all work just like they do in Dart. The +valid identifiers are: + +* `vm`: Whether the test is running on the command-line Dart VM. + +* `dartium`: Whether the test is running on Dartium. + +* `content-shell`: Whether the test is running on the headless Dartium content + shell. + +* `chrome`: Whether the test is running on Google Chrome. + +* `phantomjs`: Whether the test is running on + [PhantomJS](http://phantomjs.org/). + +* `firefox`: Whether the test is running on Mozilla Firefox. + +* `safari`: Whether the test is running on Apple Safari. + +* `ie`: Whether the test is running on Microsoft Internet Explorer. + +* `dart-vm`: Whether the test is running on the Dart VM in any context, + including Dartium. It's identical to `!js`. + +* `browser`: Whether the test is running in any browser. + +* `js`: Whether the test has been compiled to JS. This is identical to + `!dart-vm`. + +* `blink`: Whether the test is running in a browser that uses the Blink + rendering engine. + +* `windows`: Whether the test is running on Windows. If `vm` is false, this will + be `false` as well. + +* `mac-os`: Whether the test is running on Mac OS. If `vm` is false, this will + be `false` as well. + +* `linux`: Whether the test is running on Linux. If `vm` is false, this will be + `false` as well. + +* `android`: Whether the test is running on Android. If `vm` is false, this will + be `false` as well, which means that this *won't* be true if the test is + running on an Android browser. + +* `posix`: Whether the test is running on a POSIX operating system. This is + equivalent to `!windows`. + +For example, if you wanted to run a test on every browser but Chrome, you would +write `@TestOn("browser && !chrome")`. + +### Running Tests on Dartium + +Tests can be run on [Dartium][] by passing the `-p dartium` flag. If you're +using the Dart Editor, the test runner will be able to find Dartium +automatically. On Mac OS, you can also [install it using Homebrew][homebrew]. +Otherwise, make sure there's an executable called `dartium` (on Mac OS or Linux) +or `dartium.exe` (on Windows) on your system path. + +[Dartium]: https://www.dartlang.org/tools/dartium/ +[homebrew]: https://github.com/dart-lang/homebrew-dart + +Similarly, tests can be run on the headless Dartium content shell by passing `-p +content-shell`. The content shell is installed along with Dartium when using +Homebrew. Otherwise, you can downloaded it manually [from this +page][content_shell]; if you do, make sure the executable named `content_shell` +(on Mac OS or Linux) or `content_shell.exe` (on Windows) is on your system path. + +[content_shell]: http://gsdview.appspot.com/dart-archive/channels/stable/release/latest/dartium/ + +[In the future][issue 63], there will be a more explicit way to configure the +location of both the Dartium and content shell executables. + +[issue 63]: https://github.com/dart-lang/test/issues/63 + +## Asynchronous Tests + +Tests written with `async`/`await` will work automatically. The test runner +won't consider the test finished until the returned `Future` completes. + +```dart +import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("new Future.value() returns the value", () async { + var value = await new Future.value(10); + expect(value, equals(10)); + }); +} +``` + +There are also a number of useful functions and matchers for more advanced +asynchrony. The [`completion()`][completion] matcher can be used to test +`Futures`; it ensures that the test doesn't finish until the `Future` completes, +and runs a matcher against that `Future`'s value. + +[completion]: http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_completion + +```dart +import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("new Future.value() returns the value", () { + expect(new Future.value(10), completion(equals(10))); + }); +} +``` + +The [`throwsA()`][throwsA] matcher and the various `throwsExceptionType` +matchers work with both synchronous callbacks and asynchronous `Future`s. They +ensure that a particular type of exception is thrown: + +[throwsA]: http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_throwsA + +```dart +import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("new Future.error() throws the error", () { + expect(new Future.error("oh no"), throwsA(equals("oh no"))); + expect(new Future.error(new StateError("bad state")), throwsStateError); + }); +} +``` + +The [`expectAsync()`][expectAsync] function wraps another function and has two +jobs. First, it asserts that the wrapped function is called a certain number of +times, and will cause the test to fail if it's called too often; second, it +keeps the test from finishing until the function is called the requisite number +of times. + +```dart +import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("Stream.fromIterable() emits the values in the iterable", () { + var stream = new Stream.fromIterable([1, 2, 3]); + + stream.listen(expectAsync((number) { + expect(number, inInclusiveRange(1, 3)); + }, count: 3)); + }); +} +``` + +[expectAsync]: http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_expectAsync + +## Running Tests with Custom HTML + +By default, the test runner will generate its own empty HTML file for browser +tests. However, tests that need custom HTML can create their own files. These +files have three requirements: + +* They must have the same name as the test, with `.dart` replaced by `.html`. + +* They must contain a `link` tag with `rel="x-dart-test"` and an `href` + attribute pointing to the test script. + +* They must contain `<script src="packages/test/dart.js"></script>`. + +For example, if you had a test called `custom_html_test.dart`, you might write +the following HTML file: + +```html +<!doctype html> +<!-- custom_html_test.html --> +<html> + <head> + <title>Custom HTML Test</title> + <link rel="x-dart-test" href="custom_html_test.dart"> + <script src="packages/test/dart.js"></script> + </head> + <body> + // ... + </body> +</html> +``` + +## Configuring Tests + +### Skipping Tests + +If a test, group, or entire suite isn't working yet and you just want it to stop +complaining, you can mark it as "skipped". The test or tests won't be run, and, +if you supply a reason why, that reason will be printed. In general, skipping +tests indicates that they should run but is temporarily not working. If they're +is fundamentally incompatible with a platform, [`@TestOn`/`testOn`][TestOn] +should be used instead. + +[TestOn]: #restricting-tests-to-certain-platforms + +To skip a test suite, put a `@Skip` annotation at the top of the file: + +```dart +@Skip("currently failing (see issue 1234)") + +import "package:test/test.dart"; + +void main() { + // ... +} +``` + +The string you pass should describe why the test is skipped. You don't have to +include it, but it's a good idea to document why the test isn't running. + +Groups and individual tests can be skipped by passing the `skip` parameter. This +can be either `true` or a String describing why the test is skipped. For example: + +```dart +import "package:test/test.dart"; + +void main() { + group("complicated algorithm tests", () { + // ... + }, skip: "the algorithm isn't quite right"); + + test("error-checking test", () { + // ... + }, skip: "TODO: add error-checking."); +} +``` + +### Timeouts + +By default, tests will time out after 30 seconds of inactivity. However, this +can be configured on a per-test, -group, or -suite basis. To change the timeout +for a test suite, put a `@Timeout` annotation at the top of the file: + +```dart +@Timeout(const Duration(seconds: 45)) + +import "package:test/test.dart"; + +void main() { + // ... +} +``` + +In addition to setting an absolute timeout, you can set the timeout relative to +the default using `@Timeout.factor`. For example, `@Timeout.factor(1.5)` will +set the timeout to one and a half times as long as the default—45 seconds. + +Timeouts can be set for tests and groups using the `timeout` parameter. This +parameter takes a `Timeout` object just like the annotation. For example: + +```dart +import "package:test/test.dart"; + +void main() { + group("slow tests", () { + // ... + + test("even slower test", () { + // ... + }, timeout: new Timeout.factor(2)) + }, timeout: new Timeout(new Duration(minutes: 1))); +} +``` + +Nested timeouts apply in order from outermost to innermost. That means that +"even slower test" will take two minutes to time out, since it multiplies the +group's timeout by 2. + +### Platform-Specific Configuration + +Sometimes a test may need to be configured differently for different platforms. +Windows might run your code slower than other platforms, or your DOM +manipulation might not work right on Safari yet. For these cases, you can use +the `@OnPlatform` annotation and the `onPlatform` named parameter to `test()` +and `group()`. For example: + +```dart +@OnPlatform(const { + // Give Windows some extra wiggle-room before timing out. + "windows": const Timeout.factor(2) +}) + +import "package:test/test.dart"; + +void main() { + test("do a thing", () { + // ... + }, onPlatform: { + "safari": new Skip("Safari is currently broken (see #1234)") + }); +} +``` + +Both the annotation and the parameter take a map. The map's keys are [platform +selectors](#platform-selector-syntax) which describe the platforms for which the +specialized configuration applies. Its values are instances of some of the same +annotation classes that can be used for a suite: `Skip` and `Timeout`. A value +can also be a list of these values. + +If multiple platforms match, the configuration is applied in order from first to +last, just as they would in nested groups. This means that for configuration +like duration-based timeouts, the last matching value wins. + +## Testing With `barback` + +Packages using the `barback` transformer system may need to test code that's +created or modified using transformers. The test runner handles this using the +`--pub-serve` option, which tells it to load the test code from a `pub serve` +instance rather than from the filesystem. + +Before using the `--pub-serve` option, add the `test/pub_serve` transformer to +your `pubspec.yaml`. This transformer adds the necessary bootstrapping code that +allows the test runner to load your tests properly: + +```yaml +transformers: +- test/pub_serve: + $include: test/**_test{.*,}.dart +``` + +Note that if you're using the test runner along with [`polymer`][polymer], you +have to make sure that the `test/pub_serve` transformer comes *after* the +`polymer` transformer: + +[polymer]: https://www.dartlang.org/polymer/ + +```yaml +transformers: +- polymer +- test/pub_serve: + $include: test/**_test{.*,}.dart +``` + +Then, start up `pub serve`. Make sure to pay attention to which port it's using +to serve your `test/` directory: + +```shell +$ pub serve +Loading source assets... +Loading test/pub_serve transformers... +Serving my_app web on http://localhost:8080 +Serving my_app test on http://localhost:8081 +Build completed successfully +``` + +In this case, the port is `8081`. In another terminal, pass this port to +`--pub-serve` and otherwise invoke `pub run test:test` as normal: + +```shell +$ pub run test:test --pub-serve=8081 -p chrome +"pub serve" is compiling test/my_app_test.dart... +"pub serve" is compiling test/utils_test.dart... +00:00 +42: All tests passed! +```
diff --git a/pkgs/markdown/benchmark/output.html b/pkgs/markdown/benchmark/output.html new file mode 100644 index 0000000..6c49aae --- /dev/null +++ b/pkgs/markdown/benchmark/output.html
@@ -0,0 +1,364 @@ +<p><strong>TODO: Add more examples to cover all of the syntax.</strong></p> +<p>This input was taken from the test package's README to get a representative +sample of real-world markdown:</p> +<h2>Writing Tests</h2> +<p>Tests are specified using the top-level <a href="http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_test"><code>test()</code></a> function, and test +assertions are made using <a href="http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_expect"><code>expect()</code></a>:</p> +<pre class="dart"><code>import "package:test/test.dart"; + +void main() { + test("String.split() splits the string on the delimiter", () { + var string = "foo,bar,baz"; + expect(string.split(","), equals(["foo", "bar", "baz"])); + }); + + test("String.trim() removes surrounding whitespace", () { + var string = " foo "; + expect(string.trim(), equals("foo")); + }); +} +</code></pre> +<p>Tests can be grouped together using the [`group()`] function. Each group's +description is added to the beginning of its test's descriptions.</p> +<pre class="dart"><code>import "package:test/test.dart"; + +void main() { + group("String", () { + test(".split() splits the string on the delimiter", () { + var string = "foo,bar,baz"; + expect(string.split(","), equals(["foo", "bar", "baz"])); + }); + + test(".trim() removes surrounding whitespace", () { + var string = " foo "; + expect(string.trim(), equals("foo")); + }); + }); + + group("int", () { + test(".remainder() returns the remainder of division", () { + expect(11.remainder(3), equals(2)); + }); + + test(".toRadixString() returns a hex string", () { + expect(11.toRadixString(16), equals("b")); + }); + }); +} +</code></pre> +<p>Any matchers from the <a href="http://www.dartdocs.org/documentation/matcher/latest/index.html#matcher/matcher"><code>matcher</code></a> package can be used with <code>expect()</code> +to do complex validations:</p> +<pre class="dart"><code>import "package:test/test.dart"; + +void main() { + test(".split() splits the string on the delimiter", () { + expect("foo,bar,baz", allOf([ + contains("foo"), + isNot(startsWith("bar")), + endsWith("baz") + ])); + }); +} +</code></pre> +<h2>Running Tests</h2> +<p>A single test file can be run just using <code>pub run test:test path/to/test.dart</code> +(on Dart 1.10, this can be shortened to <code>pub run test path/to/test.dart</code>).</p> +<p><a href="https://raw.githubusercontent.com/dart-lang/test/master/image/test1.gif"><img alt="Single file being run via pub run"" src="https://raw.githubusercontent.com/dart-lang/test/master/image/test1.gif"></img></a></p> +<p>Many tests can be run at a time using <code>pub run test:test path/to/dir</code>.</p> +<p><a href="https://raw.githubusercontent.com/dart-lang/test/master/image/test2.gif"><img alt="Directory being run via "pub run"." src="https://raw.githubusercontent.com/dart-lang/test/master/image/test2.gif"></img></a></p> +<p>It's also possible to run a test on the Dart VM only by invoking it using <code>dart +path/to/test.dart</code>, but this doesn't load the full test runner and will be +missing some features.</p> +<p>The test runner considers any file that ends with <code>_test.dart</code> to be a test +file. If you don't pass any paths, it will run all the test files in your +<code>test/</code> directory, making it easy to test your entire application at once.</p> +<p>By default, tests are run in the Dart VM, but you can run them in the browser as +well by passing <code>pub run test:test -p chrome path/to/test.dart</code>. +<code>test</code> will take care of starting the browser and loading the tests, and all +the results will be reported on the command line just like for VM tests. In +fact, you can even run tests on both platforms with a single command: <code>pub run +test:test -p "chrome,vm" path/to/test.dart</code>.</p> +<h3>Restricting Tests to Certain Platforms</h3> +<p>Some test files only make sense to run on particular platforms. They may use +<code>dart:html</code> or <code>dart:io</code>, they might test Windows' particular filesystem +behavior, or they might use a feature that's only available in Chrome. The +<a href="#restricting-tests-to-certain-platforms"><code>@TestOn</code></a> annotation makes it easy to declare exactly which platforms +a test file should run on. Just put it at the top of your file, before any +<code>library</code> or <code>import</code> declarations:</p> +<pre class="dart"><code>@TestOn("vm") + +import "dart:io"; + +import "package:test/test.dart"; + +void main() { + // ... +} +</code></pre> +<p>The string you pass to <code>@TestOn</code> is what's called a "platform selector", and it +specifies exactly which platforms a test can run on. It can be as simple as the +name of a platform, or a more complex Dart-like boolean expression involving +these platform names.</p> +<h3>Platform Selector Syntax</h3> +<p>Platform selectors can contain identifiers, parentheses, and operators. When +loading a test, each identifier is set to <code>true</code> or <code>false</code> based on the current +platform, and the test is only loaded if the platform selector returns <code>true</code>. +The operators <code>||</code>, <code>&&</code>, <code>!</code>, and <code>? :</code> all work just like they do in Dart. The +valid identifiers are:</p><ul><li> +<p><code>vm</code>: Whether the test is running on the command-line Dart VM.</p></li><li> +<p><code>dartium</code>: Whether the test is running on Dartium.</p></li><li> +<p><code>content-shell</code>: Whether the test is running on the headless Dartium content + shell.</p></li><li> +<p><code>chrome</code>: Whether the test is running on Google Chrome.</p></li><li> +<p><code>phantomjs</code>: Whether the test is running on + <a href="http://phantomjs.org/">PhantomJS</a>.</p></li><li> +<p><code>firefox</code>: Whether the test is running on Mozilla Firefox.</p></li><li> +<p><code>safari</code>: Whether the test is running on Apple Safari.</p></li><li> +<p><code>ie</code>: Whether the test is running on Microsoft Internet Explorer.</p></li><li> +<p><code>dart-vm</code>: Whether the test is running on the Dart VM in any context, + including Dartium. It's identical to <code>!js</code>.</p></li><li> +<p><code>browser</code>: Whether the test is running in any browser.</p></li><li> +<p><code>js</code>: Whether the test has been compiled to JS. This is identical to + <code>!dart-vm</code>.</p></li><li> +<p><code>blink</code>: Whether the test is running in a browser that uses the Blink + rendering engine.</p></li><li> +<p><code>windows</code>: Whether the test is running on Windows. If <code>vm</code> is false, this will + be <code>false</code> as well.</p></li><li> +<p><code>mac-os</code>: Whether the test is running on Mac OS. If <code>vm</code> is false, this will + be <code>false</code> as well.</p></li><li> +<p><code>linux</code>: Whether the test is running on Linux. If <code>vm</code> is false, this will be + <code>false</code> as well.</p></li><li> +<p><code>android</code>: Whether the test is running on Android. If <code>vm</code> is false, this will + be <code>false</code> as well, which means that this <em>won't</em> be true if the test is + running on an Android browser.</p></li><li> +<p><code>posix</code>: Whether the test is running on a POSIX operating system. This is + equivalent to <code>!windows</code>.</p></li></ul> +<p>For example, if you wanted to run a test on every browser but Chrome, you would +write <code>@TestOn("browser && !chrome")</code>.</p> +<h3>Running Tests on Dartium</h3> +<p>Tests can be run on <a href="https://www.dartlang.org/tools/dartium/">Dartium</a> by passing the <code>-p dartium</code> flag. If you're +using the Dart Editor, the test runner will be able to find Dartium +automatically. On Mac OS, you can also <a href="https://github.com/dart-lang/homebrew-dart">install it using Homebrew</a>. +Otherwise, make sure there's an executable called <code>dartium</code> (on Mac OS or Linux) +or <code>dartium.exe</code> (on Windows) on your system path.</p> +<p>Similarly, tests can be run on the headless Dartium content shell by passing <code>-p +content-shell</code>. The content shell is installed along with Dartium when using +Homebrew. Otherwise, you can downloaded it manually <a href="http://gsdview.appspot.com/dart-archive/channels/stable/release/latest/dartium/">from this +page</a>; if you do, make sure the executable named <code>content_shell</code> +(on Mac OS or Linux) or <code>content_shell.exe</code> (on Windows) is on your system path.</p> +<p><a href="https://github.com/dart-lang/test/issues/63">In the future</a>, there will be a more explicit way to configure the +location of both the Dartium and content shell executables.</p> +<h2>Asynchronous Tests</h2> +<p>Tests written with <code>async</code>/<code>await</code> will work automatically. The test runner +won't consider the test finished until the returned <code>Future</code> completes.</p> +<pre class="dart"><code>import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("new Future.value() returns the value", () async { + var value = await new Future.value(10); + expect(value, equals(10)); + }); +} +</code></pre> +<p>There are also a number of useful functions and matchers for more advanced +asynchrony. The <a href="http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_completion"><code>completion()</code></a> matcher can be used to test +<code>Futures</code>; it ensures that the test doesn't finish until the <code>Future</code> completes, +and runs a matcher against that <code>Future</code>'s value.</p> +<pre class="dart"><code>import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("new Future.value() returns the value", () { + expect(new Future.value(10), completion(equals(10))); + }); +} +</code></pre> +<p>The <a href="http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_throwsA"><code>throwsA()</code></a> matcher and the various <code>throwsExceptionType</code> +matchers work with both synchronous callbacks and asynchronous <code>Future</code>s. They +ensure that a particular type of exception is thrown:</p> +<pre class="dart"><code>import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("new Future.error() throws the error", () { + expect(new Future.error("oh no"), throwsA(equals("oh no"))); + expect(new Future.error(new StateError("bad state")), throwsStateError); + }); +} +</code></pre> +<p>The <a href="http://www.dartdocs.org/documentation/test/latest/index.html#test/test@id_expectAsync"><code>expectAsync()</code></a> function wraps another function and has two +jobs. First, it asserts that the wrapped function is called a certain number of +times, and will cause the test to fail if it's called too often; second, it +keeps the test from finishing until the function is called the requisite number +of times.</p> +<pre class="dart"><code>import "dart:async"; + +import "package:test/test.dart"; + +void main() { + test("Stream.fromIterable() emits the values in the iterable", () { + var stream = new Stream.fromIterable([1, 2, 3]); + + stream.listen(expectAsync((number) { + expect(number, inInclusiveRange(1, 3)); + }, count: 3)); + }); +} +</code></pre> +<h2>Running Tests with Custom HTML</h2> +<p>By default, the test runner will generate its own empty HTML file for browser +tests. However, tests that need custom HTML can create their own files. These +files have three requirements:</p><ul><li> +<p>They must have the same name as the test, with <code>.dart</code> replaced by <code>.html</code>.</p></li><li> +<p>They must contain a <code>link</code> tag with <code>rel="x-dart-test"</code> and an <code>href</code> + attribute pointing to the test script.</p></li><li> +<p>They must contain <code><script src="packages/test/dart.js"></script></code>.</p></li></ul> +<p>For example, if you had a test called <code>custom_html_test.dart</code>, you might write +the following HTML file:</p> +<pre class="html"><code><!doctype html> +<!-- custom_html_test.html --> +<html> + <head> + <title>Custom HTML Test</title> + <link rel="x-dart-test" href="custom_html_test.dart"> + <script src="packages/test/dart.js"></script> + </head> + <body> + // ... + </body> +</html> +</code></pre> +<h2>Configuring Tests</h2> +<h3>Skipping Tests</h3> +<p>If a test, group, or entire suite isn't working yet and you just want it to stop +complaining, you can mark it as "skipped". The test or tests won't be run, and, +if you supply a reason why, that reason will be printed. In general, skipping +tests indicates that they should run but is temporarily not working. If they're +is fundamentally incompatible with a platform, <a href="#restricting-tests-to-certain-platforms"><code>@TestOn</code>/<code>testOn</code></a> +should be used instead.</p> +<p>To skip a test suite, put a <code>@Skip</code> annotation at the top of the file:</p> +<pre class="dart"><code>@Skip("currently failing (see issue 1234)") + +import "package:test/test.dart"; + +void main() { + // ... +} +</code></pre> +<p>The string you pass should describe why the test is skipped. You don't have to +include it, but it's a good idea to document why the test isn't running.</p> +<p>Groups and individual tests can be skipped by passing the <code>skip</code> parameter. This +can be either <code>true</code> or a String describing why the test is skipped. For example:</p> +<pre class="dart"><code>import "package:test/test.dart"; + +void main() { + group("complicated algorithm tests", () { + // ... + }, skip: "the algorithm isn't quite right"); + + test("error-checking test", () { + // ... + }, skip: "TODO: add error-checking."); +} +</code></pre> +<h3>Timeouts</h3> +<p>By default, tests will time out after 30 seconds of inactivity. However, this +can be configured on a per-test, -group, or -suite basis. To change the timeout +for a test suite, put a <code>@Timeout</code> annotation at the top of the file:</p> +<pre class="dart"><code>@Timeout(const Duration(seconds: 45)) + +import "package:test/test.dart"; + +void main() { + // ... +} +</code></pre> +<p>In addition to setting an absolute timeout, you can set the timeout relative to +the default using <code>@Timeout.factor</code>. For example, <code>@Timeout.factor(1.5)</code> will +set the timeout to one and a half times as long as the default—45 seconds.</p> +<p>Timeouts can be set for tests and groups using the <code>timeout</code> parameter. This +parameter takes a <code>Timeout</code> object just like the annotation. For example:</p> +<pre class="dart"><code>import "package:test/test.dart"; + +void main() { + group("slow tests", () { + // ... + + test("even slower test", () { + // ... + }, timeout: new Timeout.factor(2)) + }, timeout: new Timeout(new Duration(minutes: 1))); +} +</code></pre> +<p>Nested timeouts apply in order from outermost to innermost. That means that +"even slower test" will take two minutes to time out, since it multiplies the +group's timeout by 2.</p> +<h3>Platform-Specific Configuration</h3> +<p>Sometimes a test may need to be configured differently for different platforms. +Windows might run your code slower than other platforms, or your DOM +manipulation might not work right on Safari yet. For these cases, you can use +the <code>@OnPlatform</code> annotation and the <code>onPlatform</code> named parameter to <code>test()</code> +and <code>group()</code>. For example:</p> +<pre class="dart"><code>@OnPlatform(const { + // Give Windows some extra wiggle-room before timing out. + "windows": const Timeout.factor(2) +}) + +import "package:test/test.dart"; + +void main() { + test("do a thing", () { + // ... + }, onPlatform: { + "safari": new Skip("Safari is currently broken (see #1234)") + }); +} +</code></pre> +<p>Both the annotation and the parameter take a map. The map's keys are <a href="#platform-selector-syntax">platform +selectors</a> which describe the platforms for which the +specialized configuration applies. Its values are instances of some of the same +annotation classes that can be used for a suite: <code>Skip</code> and <code>Timeout</code>. A value +can also be a list of these values.</p> +<p>If multiple platforms match, the configuration is applied in order from first to +last, just as they would in nested groups. This means that for configuration +like duration-based timeouts, the last matching value wins.</p> +<h2>Testing With <code>barback</code></h2> +<p>Packages using the <code>barback</code> transformer system may need to test code that's +created or modified using transformers. The test runner handles this using the +<code>--pub-serve</code> option, which tells it to load the test code from a <code>pub serve</code> +instance rather than from the filesystem.</p> +<p>Before using the <code>--pub-serve</code> option, add the <code>test/pub_serve</code> transformer to +your <code>pubspec.yaml</code>. This transformer adds the necessary bootstrapping code that +allows the test runner to load your tests properly:</p> +<pre class="yaml"><code>transformers: +- test/pub_serve: + $include: test/**_test{.*,}.dart +</code></pre> +<p>Note that if you're using the test runner along with <a href="https://www.dartlang.org/polymer/"><code>polymer</code></a>, you +have to make sure that the <code>test/pub_serve</code> transformer comes <em>after</em> the +<code>polymer</code> transformer:</p> +<pre class="yaml"><code>transformers: +- polymer +- test/pub_serve: + $include: test/**_test{.*,}.dart +</code></pre> +<p>Then, start up <code>pub serve</code>. Make sure to pay attention to which port it's using +to serve your <code>test/</code> directory:</p> +<pre class="shell"><code>$ pub serve +Loading source assets... +Loading test/pub_serve transformers... +Serving my_app web on http://localhost:8080 +Serving my_app test on http://localhost:8081 +Build completed successfully +</code></pre> +<p>In this case, the port is <code>8081</code>. In another terminal, pass this port to +<code>--pub-serve</code> and otherwise invoke <code>pub run test:test</code> as normal:</p> +<pre class="shell"><code>$ pub run test:test --pub-serve=8081 -p chrome +"pub serve" is compiling test/my_app_test.dart... +"pub serve" is compiling test/utils_test.dart... +00:00 +42: All tests passed! +</code></pre> \ No newline at end of file
diff --git a/pkgs/markdown/lib/src/ast.dart b/pkgs/markdown/lib/src/ast.dart index 3290efb..6da7ab4 100644 --- a/pkgs/markdown/lib/src/ast.dart +++ b/pkgs/markdown/lib/src/ast.dart
@@ -2,7 +2,7 @@ // 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. -library markdown.ast; +library markdown.src.ast; typedef Node Resolver(String name); @@ -22,21 +22,21 @@ Element.empty(this.tag) : children = null, - attributes = <String, String>{}; + attributes = {}; Element.withTag(this.tag) : children = [], - attributes = <String, String>{}; + attributes = {}; Element.text(this.tag, String text) : children = [new Text(text)], - attributes = <String, String>{}; + attributes = {}; bool get isEmpty => children == null; void accept(NodeVisitor visitor) { if (visitor.visitElementBefore(this)) { - for (final child in children) child.accept(visitor); + for (var child in children) child.accept(visitor); visitor.visitElementAfter(this); } }
diff --git a/pkgs/markdown/lib/src/block_parser.dart b/pkgs/markdown/lib/src/block_parser.dart index 6a390dd..5bdc111 100644 --- a/pkgs/markdown/lib/src/block_parser.dart +++ b/pkgs/markdown/lib/src/block_parser.dart
@@ -9,43 +9,43 @@ import 'util.dart'; /// The line contains only whitespace or is empty. -final _RE_EMPTY = new RegExp(r'^(?:[ \t]*)$'); +final _emptyPattern = new RegExp(r'^(?:[ \t]*)$'); /// A series of `=` or `-` (on the next line) define setext-style headers. -final _RE_SETEXT = new RegExp(r'^(=+|-+)$'); +final _setextPattern = new RegExp(r'^(=+|-+)$'); /// Leading (and trailing) `#` define atx-style headers. /// -/// Stats with 1-6 unescaped `#` characters which must not be followed by a +/// Starts with 1-6 unescaped `#` characters which must not be followed by a /// non-space character. Line may end with any number of `#` characters,. -final _RE_HEADER = new RegExp(r'^(#{1,6})[ \x09\x0b\x0c](.*?)#*$'); +final _headerPattern = new RegExp(r'^(#{1,6})[ \x09\x0b\x0c](.*?)#*$'); /// The line starts with `>` with one optional space after. -final _RE_BLOCKQUOTE = new RegExp(r'^[ ]{0,3}>[ ]?(.*)$'); +final _blockquotePattern = new RegExp(r'^[ ]{0,3}>[ ]?(.*)$'); /// A line indented four spaces. Used for code blocks and lists. -final _RE_INDENT = new RegExp(r'^(?: |\t)(.*)$'); +final _indentPattern = new RegExp(r'^(?: |\t)(.*)$'); /// Fenced code block. -final _RE_CODE = new RegExp(r'^[ ]{0,3}(`{3,}|~{3,})(.*)$'); +final _codePattern = new RegExp(r'^[ ]{0,3}(`{3,}|~{3,})(.*)$'); /// Three or more hyphens, asterisks or underscores by themselves. Note that /// a line like `----` is valid as both HR and SETEXT. In case of a tie, /// SETEXT should win. -final _RE_HR = new RegExp(r'^ {0,3}([-*_]) *\1 *\1(?:\1| )*$'); +final _hrPattern = new RegExp(r'^ {0,3}([-*_]) *\1 *\1(?:\1| )*$'); /// Really hacky way to detect block-level embedded HTML. Just looks for /// "<somename". -final _RE_HTML = new RegExp(r'^<[ ]*\w+[ >]'); +final _htmlPattern = new RegExp(r'^<[ ]*\w+[ >]'); /// A line starting with one of these markers: `-`, `*`, `+`. May have up to /// three leading spaces before the marker and any number of spaces or tabs /// after. -final _RE_UL = new RegExp(r'^[ ]{0,3}[*+-][ \t]+(.*)$'); +final _ulPattern = new RegExp(r'^[ ]{0,3}[*+-][ \t]+(.*)$'); /// A line starting with a number like `123.`. May have up to three leading /// spaces before the marker and any number of spaces or tabs after. -final _RE_OL = new RegExp(r'^[ ]{0,3}\d+\.[ \t]+(.*)$'); +final _olPattern = new RegExp(r'^[ ]{0,3}\d+\.[ \t]+(.*)$'); /// Maintains the internal state needed to parse a series of lines into blocks /// of markdown suitable for further inline parsing. @@ -121,10 +121,10 @@ List<String> parseChildLines(BlockParser parser) { // Grab all of the lines that form the blockquote, stripping off the ">". - final childLines = <String>[]; + var childLines = <String>[]; while (!parser.isDone) { - final match = pattern.firstMatch(parser.current); + var match = pattern.firstMatch(parser.current); if (match == null) break; childLines.add(match[1]); parser.advance(); @@ -141,7 +141,7 @@ } class EmptyBlockSyntax extends BlockSyntax { - RegExp get pattern => _RE_EMPTY; + RegExp get pattern => _emptyPattern; const EmptyBlockSyntax(); @@ -160,14 +160,14 @@ bool canParse(BlockParser parser) { // Note: matches *next* line, not the current one. We're looking for the // underlining after this line. - return parser.matchesNext(_RE_SETEXT); + return parser.matchesNext(_setextPattern); } Node parse(BlockParser parser) { - final match = _RE_SETEXT.firstMatch(parser.next); + var match = _setextPattern.firstMatch(parser.next); - final tag = (match[1][0] == '=') ? 'h1' : 'h2'; - final contents = parser.document.parseInline(parser.current); + var tag = (match[1][0] == '=') ? 'h1' : 'h2'; + var contents = parser.document.parseInline(parser.current); parser.advance(); parser.advance(); @@ -177,30 +177,30 @@ /// Parses atx-style headers: `## Header ##`. class HeaderSyntax extends BlockSyntax { - RegExp get pattern => _RE_HEADER; + RegExp get pattern => _headerPattern; const HeaderSyntax(); Node parse(BlockParser parser) { - final match = pattern.firstMatch(parser.current); + var match = pattern.firstMatch(parser.current); parser.advance(); - final level = match[1].length; - final contents = parser.document.parseInline(match[2].trim()); + var level = match[1].length; + var contents = parser.document.parseInline(match[2].trim()); return new Element('h$level', contents); } } /// Parses email-style blockquotes: `> quote`. class BlockquoteSyntax extends BlockSyntax { - RegExp get pattern => _RE_BLOCKQUOTE; + RegExp get pattern => _blockquotePattern; const BlockquoteSyntax(); Node parse(BlockParser parser) { - final childLines = parseChildLines(parser); + var childLines = parseChildLines(parser); // Recursively parse the contents of the blockquote. - final children = parser.document.parseLines(childLines); + var children = parser.document.parseLines(childLines); return new Element('blockquote', children); } @@ -208,12 +208,12 @@ /// Parses preformatted code blocks that are indented four spaces. class CodeBlockSyntax extends BlockSyntax { - RegExp get pattern => _RE_INDENT; + RegExp get pattern => _indentPattern; const CodeBlockSyntax(); List<String> parseChildLines(BlockParser parser) { - final childLines = <String>[]; + var childLines = <String>[]; while (!parser.isDone) { var match = pattern.firstMatch(parser.current); @@ -239,30 +239,32 @@ } Node parse(BlockParser parser) { - final childLines = parseChildLines(parser); + var childLines = parseChildLines(parser); // The Markdown tests expect a trailing newline. childLines.add(''); // Escape the code. - final escaped = escapeHtml(childLines.join('\n')); + var escaped = escapeHtml(childLines.join('\n')); return new Element('pre', [new Element.text('code', escaped)]); } } /// Parses preformatted code blocks between two ~~~ or ``` sequences. -/// [Pandoc's markdown documentation](http://johnmacfarlane.net/pandoc/demo/example9/pandocs-markdown.html). +/// +/// See [Pandoc's documentation](http://johnmacfarlane.net/pandoc/demo/example9/pandocs-markdown.html). class FencedCodeBlockSyntax extends BlockSyntax { - RegExp get pattern => _RE_CODE; + RegExp get pattern => _codePattern; const FencedCodeBlockSyntax(); List<String> parseChildLines(BlockParser parser, [String endBlock]) { if (endBlock == null) endBlock = ''; - final childLines = <String>[]; + var childLines = <String>[]; parser.advance(); + while (!parser.isDone) { var match = pattern.firstMatch(parser.current); if (match == null || !match[1].startsWith(endBlock)) { @@ -273,6 +275,7 @@ break; } } + return childLines; } @@ -282,25 +285,24 @@ var endBlock = match.group(1); var syntax = match.group(2); - final childLines = parseChildLines(parser, endBlock); + var childLines = parseChildLines(parser, endBlock); // The Markdown tests expect a trailing newline. childLines.add(''); // Escape the code. - final escaped = escapeHtml(childLines.join('\n')); + var escaped = escapeHtml(childLines.join('\n')); var element = new Element('pre', [new Element.text('code', escaped)]); - if (syntax != '') { - element.attributes['class'] = syntax; - } + if (syntax != '') element.attributes['class'] = syntax; + return element; } } /// Parses horizontal rules like `---`, `_ _ _`, `* * *`, etc. class HorizontalRuleSyntax extends BlockSyntax { - RegExp get pattern => _RE_HR; + RegExp get pattern => _hrPattern; const HorizontalRuleSyntax(); @@ -321,17 +323,17 @@ /// 3. Absolutely no HTML parsing or validation is done. We're a markdown /// parser not an HTML parser! class BlockHtmlSyntax extends BlockSyntax { - RegExp get pattern => _RE_HTML; + RegExp get pattern => _htmlPattern; bool get canEndBlock => false; const BlockHtmlSyntax(); Node parse(BlockParser parser) { - final childLines = []; + var childLines = <String>[]; // Eat until we hit a blank line. - while (!parser.isDone && !parser.matches(_RE_EMPTY)) { + while (!parser.isDone && !parser.matches(_emptyPattern)) { childLines.add(parser.current); parser.advance(); } @@ -356,7 +358,7 @@ const ListSyntax(); Node parse(BlockParser parser) { - final items = <ListItem>[]; + var items = <ListItem>[]; var childLines = <String>[]; endItem() { @@ -373,14 +375,14 @@ } while (!parser.isDone) { - if (tryMatch(_RE_EMPTY)) { + if (tryMatch(_emptyPattern)) { // Add a blank line to the current list item. childLines.add(''); - } else if (tryMatch(_RE_UL) || tryMatch(_RE_OL)) { + } else if (tryMatch(_ulPattern) || tryMatch(_olPattern)) { // End the current list item and start a new one. endItem(); childLines.add(match[1]); - } else if (tryMatch(_RE_INDENT)) { + } else if (tryMatch(_indentPattern)) { // Strip off indent and add to current item. childLines.add(match[1]); } else if (BlockSyntax.isAtBlockEnd(parser)) { @@ -437,9 +439,9 @@ // Remove any trailing empty lines and note which items are separated by // empty lines. Do this before seeing which items are single-line so that // trailing empty lines on the last item don't force it into being a block. - for (int i = 0; i < items.length; i++) { - for (int j = items[i].lines.length - 1; j > 0; j--) { - if (_RE_EMPTY.firstMatch(items[i].lines[j]) != null) { + for (var i = 0; i < items.length; i++) { + for (var j = items[i].lines.length - 1; j > 0; j--) { + if (_emptyPattern.firstMatch(items[i].lines[j]) != null) { // Found an empty line. Item and one after it are blocks. if (i < items.length - 1) { items[i].forceBlock = true; @@ -453,22 +455,22 @@ } // Convert the list items to Nodes. - final itemNodes = <Node>[]; - for (final item in items) { - bool blockItem = item.forceBlock || (item.lines.length > 1); + var itemNodes = <Node>[]; + for (var item in items) { + var blockItem = item.forceBlock || (item.lines.length > 1); // See if it matches some block parser. - final blocksInList = [ - _RE_BLOCKQUOTE, - _RE_HEADER, - _RE_HR, - _RE_INDENT, - _RE_UL, - _RE_OL + var blocksInList = [ + _blockquotePattern, + _headerPattern, + _hrPattern, + _indentPattern, + _ulPattern, + _olPattern ]; if (!blockItem) { - for (final pattern in blocksInList) { + for (var pattern in blocksInList) { if (pattern.firstMatch(item.lines[0]) != null) { blockItem = true; break; @@ -479,11 +481,11 @@ // Parse the item as a block or inline. if (blockItem) { // Block list item. - final children = parser.document.parseLines(item.lines); + var children = parser.document.parseLines(item.lines); itemNodes.add(new Element('li', children)); } else { // Raw list item. - final contents = parser.document.parseInline(item.lines[0]); + var contents = parser.document.parseInline(item.lines[0]); itemNodes.add(new Element('li', contents)); } } @@ -494,7 +496,7 @@ /// Parses unordered lists. class UnorderedListSyntax extends ListSyntax { - RegExp get pattern => _RE_UL; + RegExp get pattern => _ulPattern; String get listTag => 'ul'; const UnorderedListSyntax(); @@ -502,7 +504,7 @@ /// Parses ordered lists. class OrderedListSyntax extends ListSyntax { - RegExp get pattern => _RE_OL; + RegExp get pattern => _olPattern; String get listTag => 'ol'; const OrderedListSyntax(); @@ -517,7 +519,7 @@ bool canParse(BlockParser parser) => true; Node parse(BlockParser parser) { - final childLines = []; + var childLines = <String>[]; // Eat until we hit something that ends a paragraph. while (!BlockSyntax.isAtBlockEnd(parser)) { @@ -525,7 +527,7 @@ parser.advance(); } - final contents = parser.document.parseInline(childLines.join('\n')); + var contents = parser.document.parseInline(childLines.join('\n')); return new Element('p', contents); } }
diff --git a/pkgs/markdown/lib/src/document.dart b/pkgs/markdown/lib/src/document.dart index 79f7c31..71a29d9 100644 --- a/pkgs/markdown/lib/src/document.dart +++ b/pkgs/markdown/lib/src/document.dart
@@ -1,4 +1,4 @@ -library markdown.document; +library markdown.src.document; import 'ast.dart'; import 'block_parser.dart'; @@ -19,16 +19,16 @@ // [id]: http:foo.com "some title" // Where there may whitespace in there, and where the title may be in // single quotes, double quotes, or parentheses. - final indent = r'^[ ]{0,3}'; // Leading indentation. - final id = r'\[([^\]]+)\]'; // Reference id in [brackets]. - final quote = r'"[^"]+"'; // Title in "double quotes". - final apos = r"'[^']+'"; // Title in 'single quotes'. - final paren = r"\([^)]+\)"; // Title in (parentheses). - final pattern = new RegExp( - '$indent$id:\\s+(\\S+)\\s*($quote|$apos|$paren|)\\s*\$'); + var indent = r'^[ ]{0,3}'; // Leading indentation. + var id = r'\[([^\]]+)\]'; // Reference id in [brackets]. + var quote = r'"[^"]+"'; // Title in "double quotes". + var apos = r"'[^']+'"; // Title in 'single quotes'. + var paren = r"\([^)]+\)"; // Title in (parentheses). + var pattern = + new RegExp('$indent$id:\\s+(\\S+)\\s*($quote|$apos|$paren|)\\s*\$'); - for (int i = 0; i < lines.length; i++) { - final match = pattern.firstMatch(lines[i]); + for (var i = 0; i < lines.length; i++) { + var match = pattern.firstMatch(lines[i]); if (match != null) { // Parse the link. var id = match[1]; @@ -57,13 +57,13 @@ /// Parse the given [lines] of markdown to a series of AST nodes. List<Node> parseLines(List<String> lines) { - final parser = new BlockParser(lines, this); + var parser = new BlockParser(lines, this); - final blocks = []; + var blocks = <Node>[]; while (!parser.isDone) { - for (final syntax in BlockSyntax.syntaxes) { + for (var syntax in BlockSyntax.syntaxes) { if (syntax.canParse(parser)) { - final block = syntax.parse(parser); + var block = syntax.parse(parser); if (block != null) blocks.add(block); break; }
diff --git a/pkgs/markdown/lib/src/html_renderer.dart b/pkgs/markdown/lib/src/html_renderer.dart index b2d96e7..edb09a0 100644 --- a/pkgs/markdown/lib/src/html_renderer.dart +++ b/pkgs/markdown/lib/src/html_renderer.dart
@@ -2,38 +2,37 @@ // 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. -library markdown.html_renderer; +library markdown.src.html_renderer; import 'ast.dart'; import 'document.dart'; import 'inline_parser.dart'; /// Converts the given string of markdown to HTML. -String markdownToHtml(String markdown, {List<InlineSyntax> inlineSyntaxes, - Resolver linkResolver, Resolver imageLinkResolver, +String markdownToHtml(String markdown, + {List<InlineSyntax> inlineSyntaxes, + Resolver linkResolver, + Resolver imageLinkResolver, bool inlineOnly: false}) { var document = new Document( inlineSyntaxes: inlineSyntaxes, imageLinkResolver: imageLinkResolver, linkResolver: linkResolver); - if (inlineOnly) { - return renderToHtml(document.parseInline(markdown)); - } else { - // Replace windows line endings with unix line endings, and split. - var lines = markdown.replaceAll('\r\n', '\n').split('\n'); - document.parseRefLinks(lines); - var blocks = document.parseLines(lines); - return renderToHtml(blocks); - } + if (inlineOnly) return renderToHtml(document.parseInline(markdown)); + + // Replace windows line endings with unix line endings, and split. + var lines = markdown.replaceAll('\r\n', '\n').split('\n'); + document.parseRefLinks(lines); + + return renderToHtml(document.parseLines(lines)); } String renderToHtml(List<Node> nodes) => new HtmlRenderer().render(nodes); /// Translates a parsed AST to HTML. class HtmlRenderer implements NodeVisitor { - static final _BLOCK_TAGS = new RegExp( - 'blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre'); + static final _blockTags = new RegExp('blockquote|h1|h2|h3|h4|h5|h6|hr|p|pre'); StringBuffer buffer; @@ -53,18 +52,17 @@ bool visitElementBefore(Element element) { // Hackish. Separate block-level elements with newlines. - if (!buffer.isEmpty && _BLOCK_TAGS.firstMatch(element.tag) != null) { + if (!buffer.isEmpty && _blockTags.firstMatch(element.tag) != null) { buffer.write('\n'); } buffer.write('<${element.tag}'); // Sort the keys so that we generate stable output. - // TODO(rnystrom): This assumes keys returns a fresh mutable - // collection. - final attributeNames = element.attributes.keys.toList(); + var attributeNames = element.attributes.keys.toList(); attributeNames.sort((a, b) => a.compareTo(b)); - for (final name in attributeNames) { + + for (var name in attributeNames) { buffer.write(' $name="${element.attributes[name]}"'); }
diff --git a/pkgs/markdown/lib/src/inline_parser.dart b/pkgs/markdown/lib/src/inline_parser.dart index 7638cdd..fe7c4dc 100644 --- a/pkgs/markdown/lib/src/inline_parser.dart +++ b/pkgs/markdown/lib/src/inline_parser.dart
@@ -2,7 +2,7 @@ // 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. -library markdown.inline_parser; +library markdown.src.inline_parser; import 'ast.dart'; import 'document.dart'; @@ -26,7 +26,6 @@ new TextSyntax(r'\s*[A-Za-z0-9]+'), // The real syntaxes. - new AutolinkSyntax(), new LinkSyntax(), new ImageLinkSyntax(), @@ -75,13 +74,15 @@ final List<TagState> _stack; InlineParser(this.source, this.document) : _stack = <TagState>[] { - /// User specified syntaxes will be the first syntaxes to be evaluated. + // User specified syntaxes are the first syntaxes to be evaluated. if (document.inlineSyntaxes != null) { syntaxes.addAll(document.inlineSyntaxes); } + syntaxes.addAll(_defaultSyntaxes); + // Custom link resolvers goes after the generic text syntax. - syntaxes.insertAll(1, <InlineSyntax>[ + syntaxes.insertAll(1, [ new LinkSyntax(linkResolver: document.linkResolver), new ImageLinkSyntax(linkResolver: document.imageLinkResolver) ]); @@ -92,25 +93,28 @@ _stack.add(new TagState(0, 0, null)); while (!isDone) { - bool matched = false; + var matched = false; // See if any of the current tags on the stack match. We don't allow tags - // of the same kind to nest, so this takes priority over other possible // matches. - for (int i = _stack.length - 1; i > 0; i--) { + // of the same kind to nest, so this takes priority over other possible + // matches. + for (var i = _stack.length - 1; i > 0; i--) { if (_stack[i].tryMatch(this)) { matched = true; break; } } + if (matched) continue; // See if the current text matches any defined markdown syntax. - for (final syntax in syntaxes) { + for (var syntax in syntaxes) { if (syntax.tryMatch(this)) { matched = true; break; } } + if (matched) continue; // If we got here, it's just text. @@ -127,17 +131,17 @@ } void writeTextRange(int start, int end) { - if (end > start) { - final text = source.substring(start, end); - final nodes = _stack.last.children; + if (end <= start) return; - // If the previous node is text too, just append. - if ((nodes.length > 0) && (nodes.last is Text)) { - final newNode = new Text('${nodes.last.text}$text'); - nodes[nodes.length - 1] = newNode; - } else { - nodes.add(new Text(text)); - } + var text = source.substring(start, end); + var nodes = _stack.last.children; + + // If the previous node is text too, just append. + if (nodes.length > 0 && nodes.last is Text) { + var textNode = nodes.last as Text; + nodes[nodes.length - 1] = new Text('${textNode.text}$text'); + } else { + nodes.add(new Text(text)); } } @@ -147,6 +151,7 @@ // TODO(rnystrom): Only need this because RegExp doesn't let you start // searching from a given offset. + @deprecated String get currentSource => source.substring(pos, source.length); bool get isDone => pos == source.length; @@ -168,16 +173,15 @@ InlineSyntax(String pattern) : pattern = new RegExp(pattern, multiLine: true); bool tryMatch(InlineParser parser) { - final startMatch = pattern.firstMatch(parser.currentSource); - if ((startMatch != null) && (startMatch.start == 0)) { + var startMatch = pattern.matchAsPrefix(parser.source, parser.pos); + if (startMatch != null) { // Write any existing plain text up to this point. parser.writeText(); - if (onMatch(parser, startMatch)) { - parser.consume(startMatch[0].length); - } + if (onMatch(parser, startMatch)) parser.consume(startMatch[0].length); return true; } + return false; } @@ -187,9 +191,10 @@ /// Matches stuff that should just be passed through as straight text. class TextSyntax extends InlineSyntax { final String substitute; + TextSyntax(String pattern, {String sub}) - : super(pattern), - substitute = sub; + : substitute = sub, + super(pattern); bool onMatch(InlineParser parser, Match match) { if (substitute == null) { @@ -210,10 +215,9 @@ // TODO(rnystrom): Make case insensitive. bool onMatch(InlineParser parser, Match match) { - final url = match[1]; - - final anchor = new Element.text('a', escapeHtml(url)) - ..attributes['href'] = url; + var url = match[1]; + var anchor = new Element.text('a', escapeHtml(url)); + anchor.attributes['href'] = url; parser.addNode(anchor); return true; @@ -226,15 +230,13 @@ final RegExp endPattern; final String tag; - TagSyntax(String pattern, {String tag, String end}) - : super(pattern), - endPattern = new RegExp((end != null) ? end : pattern, multiLine: true), - tag = tag; - // TODO(rnystrom): Doing this.field doesn't seem to work with named args. + TagSyntax(String pattern, {this.tag, String end}) + : endPattern = new RegExp((end != null) ? end : pattern, multiLine: true), + super(pattern); bool onMatch(InlineParser parser, Match match) { - parser._stack.add( - new TagState(parser.pos, parser.pos + match[0].length, this)); + parser._stack + .add(new TagState(parser.pos, parser.pos + match[0].length, this)); return true; } @@ -248,16 +250,13 @@ class LinkSyntax extends TagSyntax { final Resolver linkResolver; - /// Weather or not this link was resolved by a [Resolver] - bool resolved = false; - /// The regex for the end of a link needs to handle both reference style and /// inline styles as well as optional titles for inline links. To make that /// a bit more palatable, this breaks it into pieces. static get linkPattern { - final refLink = r'\s?\[([^\]]*)\]'; // "[id]" reflink id. - final title = r'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. - final inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. + var refLink = r'\s?\[([^\]]*)\]'; // "[id]" reflink id. + var title = r'(?:[ ]*"([^"]+)"|)'; // Optional title in quotes. + var inlineLink = '\\s?\\(([^ )]+)$title\\)'; // "(url "title")" link. return '\](?:($refLink|$inlineLink)|)'; // The groups matched by this are: @@ -277,35 +276,39 @@ // link at all. Instead, we allow users of the library to specify a special // resolver function ([linkResolver]) that may choose to handle // this. Otherwise, it's just treated as plain text. - if (isNullOrEmpty(match[1])) { + if (match[1] == null) { if (linkResolver == null) return null; - // Only allow implicit links if the content is just text. - // TODO(rnystrom): Do we want to relax this? - if (state.children.any((child) => child is! Text)) return null; - // If there are multiple children, but they are all text, send the - // combined text to linkResolver. - var textToResolve = - state.children.fold('', (oldVal, child) => oldVal + child.text); + // Treat the contents as unparsed text even if they happen to match. This + // way, we can handle things like [LINK_WITH_UNDERSCORES] as a link and + // not get confused by the emphasis. + var textToResolve = parser.source.substring(state.endPos, parser.pos); // See if we have a resolver that will generate a link for us. - resolved = true; return linkResolver(textToResolve); } else { - Link link = getLink(parser, match, state); - if (link == null) return null; - - final Element node = new Element('a', state.children) - ..attributes["href"] = escapeHtml(link.url) - ..attributes['title'] = escapeHtml(link.title); - - cleanMap(node.attributes); - return node; + return _createElement(parser, match, state); } } + /// Given that [match] has matched both a title and URL, creates an `<a>` + /// [Element] for it. + Element _createElement(InlineParser parser, Match match, TagState state) { + var link = getLink(parser, match, state); + if (link == null) return null; + + var element = new Element('a', state.children); + + element.attributes["href"] = escapeHtml(link.url); + if (link.title != null) { + element.attributes['title'] = escapeHtml(link.title); + } + + return element; + } + Link getLink(InlineParser parser, Match match, TagState state) { - if ((match[3] != null) && (match[3] != '')) { + if (match[3] != null && match[3] != '') { // Inline link like [foo](url). var url = match[3]; var title = match[4]; @@ -319,10 +322,12 @@ } else { var id; // Reference link like [foo] [bar]. - if (match[2] == '') - // The id is empty ("[]") so infer it from the contents. - id = parser.source.substring(state.startPos + 1, parser.pos); - else id = match[2]; + if (match[2] == '') { + // The id is empty ("[]") so infer it from the contents. + id = parser.source.substring(state.startPos + 1, parser.pos); + } else { + id = match[2]; + } // References are case-insensitive. id = id.toLowerCase(); @@ -331,8 +336,9 @@ } bool onMatchEnd(InlineParser parser, Match match, TagState state) { - Node node = createNode(parser, match, state); + var node = createNode(parser, match, state); if (node == null) return false; + parser.addNode(node); return true; } @@ -341,28 +347,29 @@ /// Matches images like `` and /// `![alternate text][url reference]`. class ImageLinkSyntax extends LinkSyntax { - final Resolver linkResolver; - ImageLinkSyntax({this.linkResolver}) : super(pattern: r'!\['); + ImageLinkSyntax({Resolver linkResolver}) + : super(linkResolver: linkResolver, pattern: r'!\['); - Node createNode(InlineParser parser, Match match, TagState state) { - var node = super.createNode(parser, match, state); - if (resolved) return node; - if (node == null) return null; + /// Creates an <a> element from the given complete [match]. + Element _createElement(InlineParser parser, Match match, TagState state) { + var element = super._createElement(parser, match, state); + if (element == null) return null; - final Element imageElement = new Element.withTag("img") - ..attributes["src"] = node.attributes["href"] - ..attributes["title"] = node.attributes["title"] - ..attributes["alt"] = node.children - .map((e) => isNullOrEmpty(e) || e is! Text ? '' : e.text) - .join(' '); + var image = new Element.withTag("img"); + image.attributes["src"] = element.attributes["href"]; - cleanMap(imageElement.attributes); + if (element.attributes.containsKey("title")) { + image.attributes["title"] = element.attributes["title"]; + } - node.children + var alt = element.children.map((e) => e is! Text ? "" : e.text).join(" "); + if (alt != "") image.attributes["alt"] = alt; + + element.children ..clear() - ..add(imageElement); + ..add(image); - return node; + return element; } } @@ -396,8 +403,8 @@ /// Attempts to close this tag by matching the current text against its end /// pattern. bool tryMatch(InlineParser parser) { - Match endMatch = syntax.endPattern.firstMatch(parser.currentSource); - if ((endMatch != null) && (endMatch.start == 0)) { + var endMatch = syntax.endPattern.matchAsPrefix(parser.source, parser.pos); + if (endMatch != null) { // Close the tag. close(parser, endMatch); return true; @@ -414,14 +421,14 @@ // means they are mismatched. Mismatched tags are treated as plain text in // markdown. So for each tag above this one, we write its start tag as text // and then adds its children to this one's children. - int index = parser._stack.indexOf(this); + var index = parser._stack.indexOf(this); // Remove the unmatched children. - final unmatchedTags = parser._stack.sublist(index + 1); + var unmatchedTags = parser._stack.sublist(index + 1); parser._stack.removeRange(index + 1, parser._stack.length); // Flatten them out onto this tag. - for (final unmatched in unmatchedTags) { + for (var unmatched in unmatchedTags) { // Write the start tag as text. parser.writeTextRange(unmatched.startPos, unmatched.endPos);
diff --git a/pkgs/markdown/lib/src/util.dart b/pkgs/markdown/lib/src/util.dart index 4b5f6d5..e254d26 100644 --- a/pkgs/markdown/lib/src/util.dart +++ b/pkgs/markdown/lib/src/util.dart
@@ -1,20 +1,9 @@ -library markdown.util; +library markdown.src.util; /// Replaces `<`, `&`, and `>`, with their HTML entity equivalents. String escapeHtml(String html) { - if (html == '' || html == null) return null; return html .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>'); } - -/// Removes null or empty values from [map]. -void cleanMap(Map map) { - map.keys.where((e) => isNullOrEmpty(map[e])).toList().forEach(map.remove); -} - -/// Returns true if an object is null or an empty string. -bool isNullOrEmpty(object) { - return object == null || object == ''; -}
diff --git a/pkgs/markdown/pubspec.lock b/pkgs/markdown/pubspec.lock new file mode 100644 index 0000000..b8a4fd6 --- /dev/null +++ b/pkgs/markdown/pubspec.lock
@@ -0,0 +1,131 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.26.1+3" + args: + description: args + source: hosted + version: "0.13.2" + async: + description: async + source: hosted + version: "1.3.0" + barback: + description: barback + source: hosted + version: "0.15.2+6" + charcode: + description: charcode + source: hosted + version: "1.1.0" + collection: + description: collection + source: hosted + version: "1.1.2" + crypto: + description: crypto + source: hosted + version: "0.9.0" + csslib: + description: csslib + source: hosted + version: "0.12.1" + glob: + description: glob + source: hosted + version: "1.0.5" + html: + description: html + source: hosted + version: "0.12.2" + http_multi_server: + description: http_multi_server + source: hosted + version: "1.3.2" + http_parser: + description: http_parser + source: hosted + version: "1.0.0" + logging: + description: logging + source: hosted + version: "0.11.1+1" + matcher: + description: matcher + source: hosted + version: "0.12.0+1" + mime: + description: mime + source: hosted + version: "0.9.3" + package_config: + description: package_config + source: hosted + version: "0.1.3" + path: + description: path + source: hosted + version: "1.3.6" + plugin: + description: plugin + source: hosted + version: "0.1.0" + pool: + description: pool + source: hosted + version: "1.1.0" + pub_semver: + description: pub_semver + source: hosted + version: "1.2.1" + shelf: + description: shelf + source: hosted + version: "0.6.3" + shelf_static: + description: shelf_static + source: hosted + version: "0.2.3+1" + shelf_web_socket: + description: shelf_web_socket + source: hosted + version: "0.0.1+4" + source_map_stack_trace: + description: source_map_stack_trace + source: hosted + version: "1.0.4" + source_maps: + description: source_maps + source: hosted + version: "0.10.1" + source_span: + description: source_span + source: hosted + version: "1.1.4" + stack_trace: + description: stack_trace + source: hosted + version: "1.4.1" + string_scanner: + description: string_scanner + source: hosted + version: "0.1.3+1" + test: + description: test + source: hosted + version: "0.12.4+7" + utf: + description: utf + source: hosted + version: "0.9.0+2" + watcher: + description: watcher + source: hosted + version: "0.9.7" + yaml: + description: yaml + source: hosted + version: "2.1.3"
diff --git a/pkgs/markdown/pubspec.yaml b/pkgs/markdown/pubspec.yaml index 6b4b7af..5861600 100644 --- a/pkgs/markdown/pubspec.yaml +++ b/pkgs/markdown/pubspec.yaml
@@ -1,9 +1,9 @@ name: markdown -version: 0.7.2-dev +version: 0.8.0-dev author: Dart Team <misc@dartlang.org> description: A library for converting markdown to HTML. -homepage: https://github.com/dpeek/dart-markdown +homepage: https://github.com/dart-lang/markdown environment: - sdk: '>=1.0.0 <2.0.0' + sdk: '>=1.8.0 <2.0.0' dev_dependencies: - unittest: '>=0.9.0 <0.12.0' + test: '^0.12.4+1'
diff --git a/pkgs/markdown/test/markdown_test.dart b/pkgs/markdown/test/markdown_test.dart index b41d4b2..b747289 100644 --- a/pkgs/markdown/test/markdown_test.dart +++ b/pkgs/markdown/test/markdown_test.dart
@@ -3,19 +3,25 @@ // BSD-style license that can be found in the LICENSE file. /// Unit tests for markdown. -library markdownTests; +library markdown.test.markdown_test; -import 'package:unittest/unittest.dart'; +import 'package:test/test.dart'; + import 'package:markdown/markdown.dart'; +import 'util.dart'; + /// Most of these tests are based on observing how showdown behaves: /// http://softwaremaniacs.org/playground/showdown-highlight/ void main() { group('Paragraphs', () { - validate('consecutive lines form a single paragraph', ''' + validate( + 'consecutive lines form a single paragraph', + ''' This is the first line. This is the second line. - ''', ''' + ''', + ''' <p>This is the first line. This is the second line.</p> '''); @@ -25,175 +31,247 @@ // code significantly cleaner, we should consider ourselves free to change // these tests. - validate('are terminated by a header', ''' + validate( + 'are terminated by a header', + ''' para # header - ''', ''' + ''', + ''' <p>para</p> <h1>header</h1> '''); - validate('are terminated by a setext header', ''' + validate( + 'are terminated by a setext header', + ''' para header == - ''', ''' + ''', + ''' <p>para</p> <h1>header</h1> '''); - validate('are terminated by a hr', ''' + validate( + 'are terminated by a hr', + ''' para ___ - ''', ''' + ''', + ''' <p>para</p> <hr /> '''); - validate('consume an unordered list', ''' + validate( + 'consume an unordered list', + ''' para * list - ''', ''' + ''', + ''' <p>para * list</p> '''); - validate('consume an ordered list', ''' + validate( + 'consume an ordered list', + ''' para 1. list - ''', ''' + ''', + ''' <p>para 1. list</p> '''); // Windows line endings have a \r\n format // instead of the unix \n format. - validate('take account of windows line endings', ''' + validate( + 'take account of windows line endings', + ''' line1\r\n\r\n line2\r\n - ''', ''' + ''', + ''' <p>line1</p> <p>line2</p> '''); }); group('Setext headers', () { - validate('h1', ''' + validate( + 'h1', + ''' text === - ''', ''' + ''', + ''' <h1>text</h1> '''); - validate('h2', ''' + validate( + 'h2', + ''' text --- - ''', ''' + ''', + ''' <h2>text</h2> '''); - validate('h1 on first line becomes text', ''' + validate( + 'h1 on first line becomes text', + ''' === - ''', ''' + ''', + ''' <p>===</p> '''); - validate('h2 on first line becomes text', ''' + validate( + 'h2 on first line becomes text', + ''' - - ''', ''' + ''', + ''' <p>-</p> '''); - validate('h1 turns preceding list into text', ''' + validate( + 'h1 turns preceding list into text', + ''' - list === - ''', ''' + ''', + ''' <h1>- list</h1> '''); - validate('h2 turns preceding list into text', ''' + validate( + 'h2 turns preceding list into text', + ''' - list === - ''', ''' + ''', + ''' <h1>- list</h1> '''); - validate('h1 turns preceding blockquote into text', ''' + validate( + 'h1 turns preceding blockquote into text', + ''' > quote === - ''', ''' + ''', + ''' <h1>> quote</h1> '''); - validate('h2 turns preceding blockquote into text', ''' + validate( + 'h2 turns preceding blockquote into text', + ''' > quote === - ''', ''' + ''', + ''' <h1>> quote</h1> '''); }); group('Headers', () { - validate('h1', ''' + validate( + 'h1', + ''' # header - ''', ''' + ''', + ''' <h1>header</h1> '''); - validate('h2', ''' + validate( + 'h2', + ''' ## header - ''', ''' + ''', + ''' <h2>header</h2> '''); - validate('h3', ''' + validate( + 'h3', + ''' ### header - ''', ''' + ''', + ''' <h3>header</h3> '''); - validate('h4', ''' + validate( + 'h4', + ''' #### header - ''', ''' + ''', + ''' <h4>header</h4> '''); - validate('h5', ''' + validate( + 'h5', + ''' ##### header - ''', ''' + ''', + ''' <h5>header</h5> '''); - validate('h6', ''' + validate( + 'h6', + ''' ###### header - ''', ''' + ''', + ''' <h6>header</h6> '''); - validate('h7 is not a header', ''' + validate( + 'h7 is not a header', + ''' ####### header - ''', ''' + ''', + ''' <p>####### header</p> '''); - validate('h6 must not be followed by non-space ', ''' + validate( + 'h6 must not be followed by non-space ', + ''' ######A header - ''', ''' + ''', + ''' <p>######A header</p> '''); - validate('trailing "#" are removed', ''' + validate( + 'trailing "#" are removed', + ''' # header ###### - ''', ''' + ''', + ''' <h1>header</h1> '''); }); group('Unordered lists', () { - validate('asterisk, plus and hyphen', ''' + validate( + 'asterisk, plus and hyphen', + ''' * star - dash + plus - ''', ''' + ''', + ''' <ul> <li>star</li> <li>dash</li> @@ -201,22 +279,28 @@ </ul> '''); - validate('allow numbered lines after first', ''' + validate( + 'allow numbered lines after first', + ''' * a 1. b - ''', ''' + ''', + ''' <ul> <li>a</li> <li>b</li> </ul> '''); - validate('allow a tab after the marker', ''' + validate( + 'allow a tab after the marker', + ''' *\ta +\tb -\tc 1.\td - ''', ''' + ''', + ''' <ul> <li>a</li> <li>b</li> @@ -225,23 +309,29 @@ </ul> '''); - validate('wrap items in paragraphs if blank lines separate', ''' + validate( + 'wrap items in paragraphs if blank lines separate', + ''' * one * two - ''', ''' + ''', + ''' <ul> <li><p>one</p></li> <li><p>two</p></li> </ul> '''); - validate('force paragraph on item before and after blank lines', ''' + validate( + 'force paragraph on item before and after blank lines', + ''' * one * two * three - ''', ''' + ''', + ''' <ul> <li>one</li> <li> @@ -253,24 +343,30 @@ </ul> '''); - validate('do not force paragraph if item is already block', ''' + validate( + 'do not force paragraph if item is already block', + ''' * > quote * # header - ''', ''' + ''', + ''' <ul> <li><blockquote><p>quote</p></blockquote></li> <li><h1>header</h1></li> </ul> '''); - validate('can contain multiple paragraphs', ''' + validate( + 'can contain multiple paragraphs', + ''' * one two * three - ''', ''' + ''', + ''' <ul> <li> <p>one</p> @@ -282,11 +378,14 @@ </ul> '''); - validate('can span newlines', ''' + validate( + 'can span newlines', + ''' * one two * three - ''', ''' + ''', + ''' <ul> <li> <p>one @@ -315,11 +414,14 @@ }); group('Ordered lists', () { - validate('start with numbers', ''' + validate( + 'start with numbers', + ''' 1. one 45. two 12345. three - ''', ''' + ''', + ''' <ol> <li>one</li> <li>two</li> @@ -327,10 +429,13 @@ </ol> '''); - validate('allow unordered lines after first', ''' + validate( + 'allow unordered lines after first', + ''' 1. a * b - ''', ''' + ''', + ''' <ol> <li>a</li> <li>b</li> @@ -339,30 +444,39 @@ }); group('Blockquotes', () { - validate('single line', ''' + validate( + 'single line', + ''' > blah - ''', ''' + ''', + ''' <blockquote> <p>blah</p> </blockquote> '''); - validate('with two paragraphs', ''' + validate( + 'with two paragraphs', + ''' > first > > second - ''', ''' + ''', + ''' <blockquote> <p>first</p> <p>second</p> </blockquote> '''); - validate('nested', ''' + validate( + 'nested', + ''' > one >> two > > > three - ''', ''' + ''', + ''' <blockquote> <p>one</p> <blockquote> @@ -376,32 +490,41 @@ }); group('Code blocks', () { - validate('single line', ''' + validate( + 'single line', + ''' code - ''', ''' + ''', + ''' <pre><code>code</code></pre> '''); - validate('include leading whitespace after indentation', ''' + validate( + 'include leading whitespace after indentation', + ''' zero one two three - ''', ''' + ''', + ''' <pre><code>zero one two three</code></pre> '''); - validate('code blocks separated by newlines form one block', ''' + validate( + 'code blocks separated by newlines form one block', + ''' zero one two three - ''', ''' + ''', + ''' <pre><code>zero one @@ -410,7 +533,9 @@ three</code></pre> '''); - validate('code blocks separated by two newlines form multiple blocks', ''' + validate( + 'code blocks separated by two newlines form multiple blocks', + ''' zero one @@ -419,73 +544,95 @@ three - ''', ''' + ''', + ''' <pre><code>zero one</code></pre> <pre><code>two</code></pre> <pre><code>three</code></pre> '''); - validate('escape HTML characters', ''' + validate( + 'escape HTML characters', + ''' <&> - ''', ''' + ''', + ''' <pre><code><&></code></pre> '''); }); group('Fenced code blocks', () { - validate('without an optional language identifier', ''' + validate( + 'without an optional language identifier', + ''' ``` code ``` - ''', ''' + ''', + ''' <pre><code>code </code></pre> '''); - validate('with an optional language identifier', ''' + validate( + 'with an optional language identifier', + ''' ```dart code ``` - ''', ''' + ''', + ''' <pre class="dart"><code>code </code></pre> '''); - validate('escape HTML characters', ''' + validate( + 'escape HTML characters', + ''' ``` <&> ``` - ''', ''' + ''', + ''' <pre><code><&> </code></pre> '''); - validate('Pandoc style without language identifier', ''' + validate( + 'Pandoc style without language identifier', + ''' ~~~~~ code ~~~~~ - ''', ''' + ''', + ''' <pre><code>code </code></pre> '''); - validate('Pandoc style with language identifier', ''' + validate( + 'Pandoc style with language identifier', + ''' ~~~~~dart code ~~~~~ - ''', ''' + ''', + ''' <pre class="dart"><code>code </code></pre> '''); - validate('Pandoc style with inner tildes row', ''' + validate( + 'Pandoc style with inner tildes row', + ''' ~~~~~ ~~~ code ~~~ ~~~~~ - ''', ''' + ''', + ''' <pre><code>~~~ code ~~~ @@ -494,68 +641,92 @@ }); group('Horizontal rules', () { - validate('from dashes', ''' + validate( + 'from dashes', + ''' --- - ''', ''' + ''', + ''' <hr /> '''); - validate('from asterisks', ''' + validate( + 'from asterisks', + ''' *** - ''', ''' + ''', + ''' <hr /> '''); - validate('from underscores', ''' + validate( + 'from underscores', + ''' ___ - ''', ''' + ''', + ''' <hr /> '''); - validate('can include up to two spaces', ''' + validate( + 'can include up to two spaces', + ''' _ _ _ - ''', ''' + ''', + ''' <hr /> '''); }); group('Block-level HTML', () { - validate('single line', ''' + validate( + 'single line', + ''' <table></table> - ''', ''' + ''', + ''' <table></table> '''); - validate('multi-line', ''' + validate( + 'multi-line', + ''' <table> blah </table> - ''', ''' + ''', + ''' <table> blah </table> '''); - validate('blank line ends block', ''' + validate( + 'blank line ends block', + ''' <table> blah </table> para - ''', ''' + ''', + ''' <table> blah </table> <p>para</p> '''); - validate('HTML can be bogus', ''' + validate( + 'HTML can be bogus', + ''' <bogus> blah </weird> para - ''', ''' + ''', + ''' <bogus> blah </weird> @@ -564,319 +735,466 @@ }); group('Strong', () { - validate('using asterisks', ''' + validate( + 'using asterisks', + ''' before **strong** after - ''', ''' + ''', + ''' <p>before <strong>strong</strong> after</p> '''); - validate('using underscores', ''' + validate( + 'using underscores', + ''' before __strong__ after - ''', ''' + ''', + ''' <p>before <strong>strong</strong> after</p> '''); - validate('unmatched asterisks', ''' + validate( + 'unmatched asterisks', + ''' before ** after - ''', ''' + ''', + ''' <p>before ** after</p> '''); - validate('unmatched underscores', ''' + validate( + 'unmatched underscores', + ''' before __ after - ''', ''' + ''', + ''' <p>before __ after</p> '''); - validate('multiple spans in one text', ''' + validate( + 'multiple spans in one text', + ''' a **one** b __two__ c - ''', ''' + ''', + ''' <p>a <strong>one</strong> b <strong>two</strong> c</p> '''); - validate('multi-line', ''' + validate( + 'multi-line', + ''' before **first second** after - ''', ''' + ''', + ''' <p>before <strong>first second</strong> after</p> '''); }); group('Emphasis and strong', () { - validate('single asterisks', ''' + validate( + 'single asterisks', + ''' before *em* after - ''', ''' + ''', + ''' <p>before <em>em</em> after</p> '''); - validate('single underscores', ''' + validate( + 'single underscores', + ''' before _em_ after - ''', ''' + ''', + ''' <p>before <em>em</em> after</p> '''); - validate('double asterisks', ''' + validate( + 'double asterisks', + ''' before **strong** after - ''', ''' + ''', + ''' <p>before <strong>strong</strong> after</p> '''); - validate('double underscores', ''' + validate( + 'double underscores', + ''' before __strong__ after - ''', ''' + ''', + ''' <p>before <strong>strong</strong> after</p> '''); - validate('unmatched asterisk', ''' + validate( + 'unmatched asterisk', + ''' before *after - ''', ''' + ''', + ''' <p>before *after</p> '''); - validate('unmatched underscore', ''' + validate( + 'unmatched underscore', + ''' before _after - ''', ''' + ''', + ''' <p>before _after</p> '''); - validate('multiple spans in one text', ''' + validate( + 'multiple spans in one text', + ''' a *one* b _two_ c - ''', ''' + ''', + ''' <p>a <em>one</em> b <em>two</em> c</p> '''); - validate('multi-line', ''' + validate( + 'multi-line', + ''' before *first second* after - ''', ''' + ''', + ''' <p>before <em>first second</em> after</p> '''); - validate('not processed when surrounded by spaces', ''' + validate( + 'not processed when surrounded by spaces', + ''' a * b * c _ d _ e - ''', ''' + ''', + ''' <p>a * b * c _ d _ e</p> '''); - validate('strong then emphasis', ''' + validate( + 'strong then emphasis', + ''' **strong***em* - ''', ''' + ''', + ''' <p><strong>strong</strong><em>em</em></p> '''); - validate('emphasis then strong', ''' + validate( + 'emphasis then strong', + ''' *em***strong** - ''', ''' + ''', + ''' <p><em>em</em><strong>strong</strong></p> '''); - validate('emphasis inside strong', ''' + validate( + 'emphasis inside strong', + ''' **strong *em*** - ''', ''' + ''', + ''' <p><strong>strong <em>em</em></strong></p> '''); - validate('mismatched in nested', ''' + validate( + 'mismatched in nested', + ''' *a _b* c_ - ''', ''' + ''', + ''' <p><em>a _b</em> c_</p> '''); - validate('cannot nest tags of same type', ''' + validate( + 'cannot nest tags of same type', + ''' *a _b *c* d_ e* - ''', ''' + ''', + ''' <p><em>a _b </em>c<em> d_ e</em></p> '''); }); group('Inline code', () { - validate('simple case', ''' + validate( + 'simple case', + ''' before `source` after - ''', ''' + ''', + ''' <p>before <code>source</code> after</p> '''); - validate('unmatched backtick', ''' + validate( + 'unmatched backtick', + ''' before ` after - ''', ''' + ''', + ''' <p>before ` after</p> '''); - validate('multiple spans in one text', ''' + validate( + 'multiple spans in one text', + ''' a `one` b `two` c - ''', ''' + ''', + ''' <p>a <code>one</code> b <code>two</code> c</p> '''); - validate('multi-line', ''' + validate( + 'multi-line', + ''' before `first second` after - ''', ''' + ''', + ''' <p>before <code>first second</code> after</p> '''); - validate('simple double backticks', ''' + validate( + 'simple double backticks', + ''' before ``source`` after - ''', ''' + ''', + ''' <p>before <code>source</code> after</p> '''); - validate('double backticks', ''' + validate( + 'double backticks', + ''' before ``can `contain` backticks`` after - ''', ''' + ''', + ''' <p>before <code>can `contain` backticks</code> after</p> '''); - validate('double backticks with spaces', ''' + validate( + 'double backticks with spaces', + ''' before `` `tick` `` after - ''', ''' + ''', + ''' <p>before <code>`tick`</code> after</p> '''); - validate('multiline double backticks with spaces', ''' + validate( + 'multiline double backticks with spaces', + ''' before ``in `tick` another`` after - ''', ''' + ''', + ''' <p>before <code>in `tick` another</code> after</p> '''); - validate('ignore markup inside code', ''' + validate( + 'ignore markup inside code', + ''' before `*b* _c_` after - ''', ''' + ''', + ''' <p>before <code>*b* _c_</code> after</p> '''); - validate('escape HTML characters', ''' + validate( + 'escape HTML characters', + ''' `<&>` - ''', ''' + ''', + ''' <p><code><&></code></p> '''); - validate('escape HTML tags', ''' + validate( + 'escape HTML tags', + ''' '*' `<em>` - ''', ''' + ''', + ''' <p>'*' <code><em></code></p> '''); }); group('HTML encoding', () { - validate('less than and ampersand are escaped', ''' + validate( + 'less than and ampersand are escaped', + ''' < & - ''', ''' + ''', + ''' <p>< &</p> '''); - validate('greater than is not escaped', ''' + validate( + 'greater than is not escaped', + ''' not you > - ''', ''' + ''', + ''' <p>not you ></p> '''); - validate('existing entities are untouched', ''' + validate( + 'existing entities are untouched', + ''' & - ''', ''' + ''', + ''' <p>&</p> '''); }); group('Autolinks', () { - validate('basic link', ''' + validate( + 'basic link', + ''' before <http://foo.com/> after - ''', ''' + ''', + ''' <p>before <a href="http://foo.com/">http://foo.com/</a> after</p> '''); - validate('handles ampersand in url', ''' + validate( + 'handles ampersand in url', + ''' <http://foo.com/?a=1&b=2> - ''', ''' + ''', + ''' <p><a href="http://foo.com/?a=1&b=2">http://foo.com/?a=1&b=2</a></p> '''); }); group('Reference links', () { - validate('double quotes for title', ''' + validate( + 'double quotes for title', + ''' links [are] [a] awesome [a]: http://foo.com "woo" - ''', ''' + ''', + ''' <p>links <a href="http://foo.com" title="woo">are</a> awesome</p> '''); - validate('single quoted title', """ + validate( + 'single quoted title', + """ links [are] [a] awesome [a]: http://foo.com 'woo' - """, ''' + """, + ''' <p>links <a href="http://foo.com" title="woo">are</a> awesome</p> '''); - validate('parentheses for title', ''' + validate( + 'parentheses for title', + ''' links [are] [a] awesome [a]: http://foo.com (woo) - ''', ''' + ''', + ''' <p>links <a href="http://foo.com" title="woo">are</a> awesome</p> '''); - validate('no title', ''' + validate( + 'no title', + ''' links [are] [a] awesome [a]: http://foo.com - ''', ''' + ''', + ''' <p>links <a href="http://foo.com">are</a> awesome</p> '''); - validate('unknown link becomes plaintext', ''' + validate( + 'unknown link becomes plaintext', + ''' [not] [known] - ''', ''' + ''', + ''' <p>[not] [known]</p> '''); - validate('can style link contents', ''' + validate( + 'can style link contents', + ''' links [*are*] [a] awesome [a]: http://foo.com - ''', ''' + ''', + ''' <p>links <a href="http://foo.com"><em>are</em></a> awesome</p> '''); - validate('inline styles after a bad link are processed', ''' + validate( + 'inline styles after a bad link are processed', + ''' [bad] `code` - ''', ''' + ''', + ''' <p>[bad] <code>code</code></p> '''); - validate('empty reference uses text from link', ''' + validate( + 'empty reference uses text from link', + ''' links [are][] awesome [are]: http://foo.com - ''', ''' + ''', + ''' <p>links <a href="http://foo.com">are</a> awesome</p> '''); - validate('references are case-insensitive', ''' + validate( + 'references are case-insensitive', + ''' links [ARE][] awesome [are]: http://foo.com - ''', ''' + ''', + ''' <p>links <a href="http://foo.com">ARE</a> awesome</p> '''); }); group('Inline links', () { - validate('double quotes for title', ''' + validate( + 'double quotes for title', + ''' links [are](http://foo.com "woo") awesome - ''', ''' + ''', + ''' <p>links <a href="http://foo.com" title="woo">are</a> awesome</p> '''); - validate('no title', ''' + validate( + 'no title', + ''' links [are] (http://foo.com) awesome - ''', ''' + ''', + ''' <p>links <a href="http://foo.com">are</a> awesome</p> '''); - validate('can style link contents', ''' + validate( + 'can style link contents', + ''' links [*are*](http://foo.com) awesome - ''', ''' + ''', + ''' <p>links <a href="http://foo.com"><em>are</em></a> awesome</p> '''); }); group('Inline Images', () { - validate('image', ''' + validate( + 'image', + '''  - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png"> <img src="http://foo.com/foo.png"></img> @@ -884,9 +1202,12 @@ </p> '''); - validate('alternate text', ''' + validate( + 'alternate text', + '''  - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png"> <img alt="alternate text" src="http://foo.com/foo.png"></img> @@ -894,18 +1215,24 @@ </p> '''); - validate('title', ''' + validate( + 'title', + '''  - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png" title="optional title"> <img src="http://foo.com/foo.png" title="optional title"></img> </a> </p> '''); - validate('invalid alt text', ''' + validate( + 'invalid alt text', + '''  - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png"> <img src="http://foo.com/foo.png"></img> @@ -915,10 +1242,13 @@ }); group('Reference Images', () { - validate('image', ''' + validate( + 'image', + ''' ![][foo] [foo]: http://foo.com/foo.png - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png"> <img src="http://foo.com/foo.png"></img> @@ -926,10 +1256,13 @@ </p> '''); - validate('alternate text', ''' + validate( + 'alternate text', + ''' ![alternate text][foo] [foo]: http://foo.com/foo.png - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png"> <img alt="alternate text" src="http://foo.com/foo.png"></img> @@ -937,10 +1270,13 @@ </p> '''); - validate('title', ''' + validate( + 'title', + ''' ![][foo] [foo]: http://foo.com/foo.png "optional title" - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png" title="optional title"> <img src="http://foo.com/foo.png" title="optional title"></img> @@ -948,10 +1284,13 @@ </p> '''); - validate('invalid alt text', ''' + validate( + 'invalid alt text', + ''' ![`alt`][foo] [foo]: http://foo.com/foo.png "optional title" - ''', ''' + ''', + ''' <p> <a href="http://foo.com/foo.png" title="optional title"> <img src="http://foo.com/foo.png" title="optional title"></img> @@ -961,33 +1300,53 @@ }); group('Resolver', () { - var nyanResolver = (text) => new Text('~=[,,_${text}_,,]:3'); - validate('simple link resolver', ''' + Node nyanResolver(String text) => new Text('~=[,,_${text}_,,]:3'); + + validate( + 'simple link resolver', + ''' resolve [this] thing - ''', ''' + ''', + ''' <p>resolve ~=[,,_this_,,]:3 thing</p> - ''', linkResolver: nyanResolver); - validate('simple image resolver', ''' + ''', + linkResolver: nyanResolver); + validate( + 'simple image resolver', + ''' resolve ![this] thing - ''', ''' + ''', + ''' <p>resolve ~=[,,_this_,,]:3 thing</p> - ''', imageLinkResolver: nyanResolver); + ''', + imageLinkResolver: nyanResolver); + + validate( + 'can resolve link containing inline tags', + ''' + resolve [*star* _underline_] thing + ''', + ''' + <p>resolve ~=[,,_*star* _underline__,,]:3 thing</p> + ''', + linkResolver: nyanResolver); }); group('Custom inline syntax', () { - List<InlineSyntax> nyanSyntax = [ - new TextSyntax('nyan', sub: '~=[,,_,,]:3') - ]; - validate('simple inline syntax', ''' + validate( + 'simple inline syntax', + ''' nyan - ''', ''' + ''', + ''' <p>~=[,,_,,]:3</p> - ''', inlineSyntaxes: nyanSyntax); + ''', + inlineSyntaxes: [new TextSyntax('nyan', sub: '~=[,,_,,]:3')]); validate('dart custom links', 'links [are<foo>] awesome', '<p>links <a>are<foo></a> awesome</p>', - linkResolver: (text) => new Element.text( - 'a', text.replaceAll('<', '<'))); + linkResolver: (text) => + new Element.text('a', text.replaceAll('<', '<'))); // TODO(amouravski): need more tests here for custom syntaxes, as some // things are not quite working properly. The regexps are sometime a little @@ -995,129 +1354,70 @@ }); group('Inline only', () { - validate('simple line', ''' + validate( + 'simple line', + ''' This would normally create a paragraph. - ''', ''' + ''', + ''' This would normally create a paragraph. - ''', inlineOnly: true); - validate('strong and em', ''' + ''', + inlineOnly: true); + validate( + 'strong and em', + ''' This would _normally_ create a **paragraph**. - ''', ''' + ''', + ''' This would <em>normally</em> create a <strong>paragraph</strong>. - ''', inlineOnly: true); - validate('link', ''' + ''', + inlineOnly: true); + validate( + 'link', + ''' This [link](http://www.example.com/) will work normally. - ''', ''' + ''', + ''' This <a href="http://www.example.com/">link</a> will work normally. - ''', inlineOnly: true); - validate('references do not work', ''' + ''', + inlineOnly: true); + validate( + 'references do not work', + ''' [This][] shouldn't work, though. - ''', ''' + ''', + ''' [This][] shouldn't work, though. - ''', inlineOnly: true); - validate('less than and ampersand are escaped', ''' + ''', + inlineOnly: true); + validate( + 'less than and ampersand are escaped', + ''' < & - ''', ''' + ''', + ''' < & - ''', inlineOnly: true); - validate('keeps newlines', ''' + ''', + inlineOnly: true); + validate( + 'keeps newlines', + ''' This paragraph continues after a newline. - ''', ''' + ''', + ''' This paragraph continues after a newline. - ''', inlineOnly: true); - validate('ignores block-level markdown syntax', ''' + ''', + inlineOnly: true); + validate( + 'ignores block-level markdown syntax', + ''' 1. This will not be an <ol>. - ''', ''' + ''', + ''' 1. This will not be an <ol>. - ''', inlineOnly: true); + ''', + inlineOnly: true); }); } - -/** - * Removes eight spaces of leading indentation from a multiline string. - * - * Note that this is very sensitive to how the literals are styled. They should - * be: - * ''' - * Text starts on own line. Lines up with subsequent lines. - * Lines are indented exactly 8 characters from the left margin.''' - * - * This does nothing if text is only a single line. - */ -// TODO(nweiz): Make this auto-detect the indentation level from the first -// non-whitespace line. -String cleanUpLiteral(String text) { - var lines = text.split('\n'); - if (lines.length <= 1) return text; - - for (var j = 0; j < lines.length; j++) { - if (lines[j].length > 8) { - lines[j] = lines[j].substring(8, lines[j].length); - } else { - lines[j] = ''; - } - } - - return lines.join('\n'); -} - -void validate(String description, String markdown, String html, - {List<InlineSyntax> inlineSyntaxes, - Resolver linkResolver, Resolver imageLinkResolver, - bool inlineOnly: false}) { - test(description, () { - markdown = cleanUpLiteral(markdown); - html = cleanUpLiteral(html); - - var result = markdownToHtml(markdown, - inlineSyntaxes: inlineSyntaxes, - linkResolver: linkResolver, - imageLinkResolver: imageLinkResolver, - inlineOnly: inlineOnly); - var passed = compareOutput(html, result); - - if (!passed) { - // Remove trailing newline. - html = html.substring(0, html.length - 1); - - var sb = new StringBuffer(); - sb.writeln('Expected: ${html.replaceAll("\n", "\n ")}'); - sb.writeln(' Actual: ${result.replaceAll("\n", "\n ")}'); - - fail(sb.toString()); - } - }); -} - -/// Does a loose comparison of the two strings of HTML. Ignores differences in -/// newlines and indentation. -bool compareOutput(String a, String b) { - int i = 0; - int j = 0; - - skipIgnored(String s, int i) { - // Ignore newlines. - while ((i < s.length) && (s[i] == '\n')) { - i++; - // Ignore indentation. - while ((i < s.length) && (s[i] == ' ')) i++; - } - - return i; - } - - while (true) { - i = skipIgnored(a, i); - j = skipIgnored(b, j); - - // If one string runs out of non-ignored strings, the other must too. - if (i == a.length) return j == b.length; - if (j == b.length) return i == a.length; - - if (a[i] != b[j]) return false; - i++; - j++; - } -}
diff --git a/pkgs/markdown/test/util.dart b/pkgs/markdown/test/util.dart new file mode 100644 index 0000000..d9a6e38 --- /dev/null +++ b/pkgs/markdown/test/util.dart
@@ -0,0 +1,95 @@ +// Copyright (c) 2015, 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. + +library markdown.test.utils; + +import 'package:test/test.dart'; + +import 'package:markdown/markdown.dart'; + +/// Removes eight spaces of leading indentation from a multiline string. +/// +/// Note that this is very sensitive to how the literals are styled. They should +/// be: +/// ''' +/// Text starts on own line. Lines up with subsequent lines. +/// Lines are indented exactly 8 characters from the left margin.''' +/// +/// This does nothing if text is only a single line. +// TODO(nweiz): Make this auto-detect the indentation level from the first +// non-whitespace line. +String cleanUpLiteral(String text) { + var lines = text.split('\n'); + if (lines.length <= 1) return text; + + for (var j = 0; j < lines.length; j++) { + if (lines[j].length > 8) { + lines[j] = lines[j].substring(8, lines[j].length); + } else { + lines[j] = ''; + } + } + + return lines.join('\n'); +} + +void validate(String description, String markdown, String html, + {List<InlineSyntax> inlineSyntaxes, + Resolver linkResolver, + Resolver imageLinkResolver, + bool inlineOnly: false}) { + test(description, () { + markdown = cleanUpLiteral(markdown); + html = cleanUpLiteral(html); + + var result = markdownToHtml(markdown, + inlineSyntaxes: inlineSyntaxes, + linkResolver: linkResolver, + imageLinkResolver: imageLinkResolver, + inlineOnly: inlineOnly); + var passed = compareOutput(html, result); + + if (!passed) { + // Remove trailing newline. + html = html.substring(0, html.length - 1); + + var sb = new StringBuffer(); + sb.writeln('Expected: ${html.replaceAll("\n", "\n ")}'); + sb.writeln(' Actual: ${result.replaceAll("\n", "\n ")}'); + + fail(sb.toString()); + } + }); +} + +/// Does a loose comparison of the two strings of HTML. Ignores differences in +/// newlines and indentation. +bool compareOutput(String a, String b) { + int i = 0; + int j = 0; + + skipIgnored(String s, int i) { + // Ignore newlines. + while ((i < s.length) && (s[i] == '\n')) { + i++; + // Ignore indentation. + while ((i < s.length) && (s[i] == ' ')) i++; + } + + return i; + } + + while (true) { + i = skipIgnored(a, i); + j = skipIgnored(b, j); + + // If one string runs out of non-ignored strings, the other must too. + if (i == a.length) return j == b.length; + if (j == b.length) return i == a.length; + + if (a[i] != b[j]) return false; + i++; + j++; + } +}