refactor: refactoring common utils, routes > routing, db drivers

This commit is contained in:
Patrick Stewart 2024-12-14 20:56:05 -07:00
parent 2258f301e8
commit e25f672a40
336 changed files with 33106 additions and 49 deletions

View file

@ -1,8 +1,11 @@
name: protevus_platform name: protevus_platform
repository: https://github.com/protevus/platform repository: https://github.com/protevus/platform
packages: packages:
- apps/** - common/**
- drivers/**
- packages/** - packages/**
- incubation/**
- apps/**
- helpers/tools/** - helpers/tools/**
- examples/** - examples/**

View file

@ -0,0 +1,69 @@
# Change Log
## 5.3.0
* Require Dart >= 3.3
* Updated `platform_http_server` to 4.4.0
* Updated `lints` to 4.0.0
## 5.2.0
* Updated `lints` to 3.0.0
* Updated `platform_http_server` to 4.2.0
## 5.1.0
* Updated `platform_http_server` to 4.1.1
* Updated `http` to 1.0.0
## 5.0.0
* Require Dart >= 3.0
## 5.0.0-beta.1
* Require Dart >= 3.0
* Updated `platform_http_server` to 4.0.0
## 4.0.1
* Updated `platform_http_server` to 3.0.0
## 4.0.0
* Require Dart >= 2.17
## 3.0.1
* Fixed boken license link
## 3.0.0
* Upgraded from `pendantic` to `lints` linter
* Published as `platform_body_parser` package
* Fixed linter warnings
## 2.1.1
* Fixed calling deprecated methods in unit test
## 2.1.0
* Replaced `http_server` with `platform_http_server`
## 2.0.1
* Fixed source code formating warning
* Updated README
## 2.0.0
* Migrated to support Dart SDK 2.12.x NNBD
## 1.1.1
* Dart 2 updates; should fix Angel in Travis.
## 1.1.0
* Add `parseBodyFromStream`

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,73 @@
# Belatuk Body Parser
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/platform_body_parser?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/blob/main/packages/body_parser/LICENSE)
**Replacement of `package:body_parser` with breaking changes to support NNBD.**
Parse request bodies and query strings in Dart, as well multipart/form-data uploads. No external dependencies required.
This is the request body parser powering the [Angel3 framework](https://pub.dev/packages/angel3_framework). If you are looking for a server-side solution with dependency injection, WebSockets, and more, then I highly recommend it as your first choice. Bam!
## Contents
- [Belatuk Body Parser](#belatuk-body-parser)
- [Contents](#contents)
- [About](#about)
- [Installation](#installation)
- [Usage](#usage)
- [Custom Body Parsing](#custom-body-parsing)
### About
This package is similar to Express.js's `body-parser` module. It fully supports JSON, x-www-form-urlencoded as well as query strings requests. You can also include arrays in your query, in the same way you would for a PHP application. A benefit of this is that primitive types are automatically deserialized correctly. As in, if you have a `hello=1.5` request, then `body['hello']` will equal `1.5` and not `'1.5'`.
### Installation
To install Body Parser for your Dart project, simply add body_parser to your pub dependencies.
dependencies:
platform_body_parser: ^5.2.0
### Usage
Body Parser exposes a simple class called `BodyParseResult`. You can easily parse the query string and request body for a request by calling `Future<BodyParseResult> parseBody`.
```dart
import 'dart:convert';
import 'package:platform_body_parser/platform_body_parser.dart';
main() async {
// ...
await for (HttpRequest request in server) {
request.response.write(JSON.encode(await parseBody(request).body));
await request.response.close();
}
}
```
You can also use `buildMapFromUri(Map, String)` to populate a map from a URL encoded string.
This can easily be used with a library like [Angel3 JSON God](https://pub.dev/packages/angel3_json_god) to build structured JSON/REST APIs. Add validation and you've got an instant backend.
```dart
MyClass create(HttpRequest request) async {
return god.deserialize(await parseBody(request).body, MyClass);
}
```
### Custom Body Parsing
In cases where you need to parse unrecognized content types, `body_parser` won't be of any help to you on its own. However, you can use the `originalBuffer` property of a `BodyParseResult` to see the original request buffer. To get this functionality, pass `storeOriginalBuffer` as `true` when calling `parseBody`.
For example, if you wanted to [parse GraphQL queries within your server](https://github.com/dukefirehawk/graphql_dart)...
```dart
app.get('/graphql', (req, res) async {
if (req.headers.contentType.mimeType == 'application/graphql') {
var graphQlString = String.fromCharCodes(req.originalBuffer);
// ...
}
});
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,61 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:http_parser/http_parser.dart';
import 'package:platform_body_parser/body_parser.dart';
void main() async {
var address = '127.0.0.1';
var port = 3000;
var futures = <Future>[];
for (var i = 1; i < Platform.numberOfProcessors; i++) {
futures.add(Isolate.spawn(start, [address, port, i]));
}
await Future.wait(futures).then((_) {
print('All instances started.');
print(
'Test with "wrk -t12 -c400 -d30s -s ./example/post.lua http://localhost:3000" or similar');
start([address, port, 0]);
});
}
void start(List args) {
var address = InternetAddress(args[0] as String);
var port = 8080;
if (args[1] is int) {
args[1];
}
var id = 0;
if (args[2] is int) {
args[2];
}
HttpServer.bind(address, port, shared: true).then((server) {
server.listen((request) async {
// ignore: deprecated_member_use
var body = await defaultParseBody(request);
request.response
..headers.contentType = ContentType('application', 'json')
..write(json.encode(body.body));
await request.response.close();
});
print(
'Server #$id listening at http://${server.address.address}:${server.port}');
});
}
Future<BodyParseResult> defaultParseBody(HttpRequest request,
{bool storeOriginalBuffer = false}) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: storeOriginalBuffer);
}

View file

@ -0,0 +1,6 @@
-- example HTTP POST script which demonstrates setting the
-- HTTP method, body, and adding a header
wrk.method = "POST"
wrk.body = "foo=bar&baz=quux"
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

View file

@ -0,0 +1,6 @@
/// A library for parsing HTTP request bodies and queries.
library platform_body_parser;
export 'src/body_parse_result.dart';
export 'src/file_upload_info.dart';
export 'src/parse_body.dart';

View file

@ -0,0 +1,28 @@
import 'file_upload_info.dart';
/// A representation of data from an incoming request.
abstract class BodyParseResult {
/// The parsed body.
Map<String?, dynamic> get body;
/// The parsed query string.
Map<String?, dynamic> get query;
/// All files uploaded within this request.
List<FileUploadInfo> get files;
/// The original body bytes sent with this request.
///
/// You must set [storeOriginalBuffer] to `true` to see this.
List<int>? get originalBuffer;
/// If an error was encountered while parsing the body, it will appear here.
///
/// Otherwise, this is `null`.
dynamic get error;
/// If an error was encountered while parsing the body, the call stack will appear here.
///
/// Otherwise, this is `null`.
StackTrace? get stack;
}

View file

@ -0,0 +1,7 @@
import 'file_upload_info.dart';
List<FileUploadInfo> getFileDataFromChunk(
String chunk, String boundary, String fileUploadName, Map body) {
var result = <FileUploadInfo>[];
return result;
}

View file

@ -0,0 +1,17 @@
/// Represents a file uploaded to the server.
class FileUploadInfo {
/// The MIME type of the uploaded file.
String? mimeType;
/// The name of the file field from the request.
String? name;
/// The filename of the file.
String? filename;
/// The bytes that make up this file.
List<int> data;
FileUploadInfo(
{this.mimeType, this.name, this.filename, this.data = const []});
}

View file

@ -0,0 +1,22 @@
import 'dart:convert';
dynamic getValue(String value) {
try {
var numValue = num.parse(value);
if (!numValue.isNaN) {
return numValue;
} else {
return value;
}
} on FormatException {
if (value.startsWith('[') && value.endsWith(']')) {
return json.decode(value);
} else if (value.startsWith('{') && value.endsWith('}')) {
return json.decode(value);
} else if (value.trim().toLowerCase() == 'null') {
return null;
} else {
return value;
}
}
}

View file

@ -0,0 +1,44 @@
import 'get_value.dart';
/// Parses a URI-encoded string into real data! **Wow!**
///
/// Whichever map you provide will be automatically populated from the urlencoded body string you provide.
void buildMapFromUri(Map map, String body) {
var parseArrayRgx = RegExp(r'^(.+)\[\]$');
for (var keyValuePair in body.split('&')) {
if (keyValuePair.contains('=')) {
var equals = keyValuePair.indexOf('=');
var key = Uri.decodeQueryComponent(keyValuePair.substring(0, equals));
var value = Uri.decodeQueryComponent(keyValuePair.substring(equals + 1));
if (parseArrayRgx.hasMatch(key)) {
Match queryMatch = parseArrayRgx.firstMatch(key)!;
key = queryMatch.group(1)!;
if (map[key] is! List) {
map[key] = [];
}
map[key].add(getValue(value));
} else if (key.contains('.')) {
// i.e. map.foo.bar => [map, foo, bar]
var keys = key.split('.');
var targetMap = map[keys[0]] != null ? map[keys[0]] as Map? : {};
map[keys[0]] = targetMap;
for (var i = 1; i < keys.length; i++) {
if (i < keys.length - 1) {
targetMap![keys[i]] = targetMap[keys[i]] ?? {};
targetMap = targetMap[keys[i]] as Map?;
} else {
targetMap![keys[i]] = getValue(value);
}
}
} else {
map[key] = getValue(value);
}
} else {
map[Uri.decodeQueryComponent(keyValuePair)] = true;
}
}
}

View file

@ -0,0 +1,147 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:http_parser/http_parser.dart';
import 'package:platform_http_server/http_server.dart';
import 'package:mime/mime.dart';
import 'body_parse_result.dart';
import 'file_upload_info.dart';
import 'map_from_uri.dart';
/// Forwards to [parseBodyFromStream].
@Deprecated("parseBodyFromStream")
Future<BodyParseResult> parseBody(HttpRequest request,
{bool storeOriginalBuffer = false}) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: storeOriginalBuffer);
}
/// Grabs data from an incoming request.
///
/// Supports URL-encoded and JSON, as well as multipart/* forms.
/// On a file upload request, only fields with the name **'file'** are processed
/// as files. Anything else is put in the body. You can change the upload file name
/// via the *fileUploadName* parameter. :)
///
/// Use [storeOriginalBuffer] to add the original request bytes to the result.
Future<BodyParseResult> parseBodyFromStream(
Stream<Uint8List> data, MediaType? contentType, Uri requestUri,
{bool storeOriginalBuffer = false}) async {
var result = _BodyParseResultImpl();
Future<Uint8List> getBytes() {
return data
.fold<BytesBuilder>(BytesBuilder(copy: false), (a, b) => a..add(b))
.then((b) => b.takeBytes());
}
Future<String> getBody() {
if (storeOriginalBuffer) {
return getBytes().then((bytes) {
result.originalBuffer = bytes;
return utf8.decode(bytes);
});
} else {
return utf8.decoder.bind(data).join();
}
}
try {
if (contentType != null) {
if (contentType.type == 'multipart' &&
contentType.parameters.containsKey('boundary')) {
Stream<Uint8List> stream;
if (storeOriginalBuffer) {
var bytes = result.originalBuffer = await getBytes();
var ctrl = StreamController<Uint8List>()..add(bytes);
await ctrl.close();
stream = ctrl.stream;
} else {
stream = data;
}
var parts =
MimeMultipartTransformer(contentType.parameters['boundary']!)
.bind(stream)
.map((part) =>
HttpMultipartFormData.parse(part, defaultEncoding: utf8));
await for (HttpMultipartFormData part in parts) {
if (part.isBinary ||
part.contentDisposition.parameters.containsKey('filename')) {
var builder = await part.fold(
BytesBuilder(copy: false),
(BytesBuilder b, d) =>
b..add(d is! String ? (d as List<int>?)! : d.codeUnits));
var upload = FileUploadInfo(
mimeType: part.contentType!.mimeType,
name: part.contentDisposition.parameters['name'],
filename:
part.contentDisposition.parameters['filename'] ?? 'file',
data: builder.takeBytes());
result.files.add(upload);
} else if (part.isText) {
var text = await part.join();
buildMapFromUri(result.body,
'${part.contentDisposition.parameters["name"]}=$text');
}
}
} else if (contentType.mimeType == 'application/json') {
result.body.addAll(
_foldToStringDynamic(json.decode(await getBody()) as Map?)!);
} else if (contentType.mimeType == 'application/x-www-form-urlencoded') {
var body = await getBody();
buildMapFromUri(result.body, body);
} else if (storeOriginalBuffer == true) {
result.originalBuffer = await getBytes();
}
} else {
if (requestUri.hasQuery) {
buildMapFromUri(result.query, requestUri.query);
}
if (storeOriginalBuffer == true) {
result.originalBuffer = await getBytes();
}
}
} catch (e, st) {
result.error = e;
result.stack = st;
}
return result;
}
class _BodyParseResultImpl implements BodyParseResult {
@override
Map<String?, dynamic> body = {};
@override
List<FileUploadInfo> files = [];
@override
List<int>? originalBuffer;
@override
Map<String?, dynamic> query = {};
@override
dynamic error;
@override
StackTrace? stack;
}
Map<String, dynamic>? _foldToStringDynamic(Map? map) {
return map?.keys.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
}

View file

@ -0,0 +1,417 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
http:
dependency: "direct dev"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: "direct main"
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: "direct main"
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform_http_server:
dependency: "direct main"
description:
path: "../http_server"
relative: true
source: path
version: "4.5.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test:
dependency: "direct dev"
description:
name: test
sha256: "22eb7769bee38c7e032d532e8daa2e1cc901b799f603550a4db8f3a5f5173ea2"
url: "https://pub.dev"
source: hosted
version: "1.25.12"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"

View file

@ -0,0 +1,14 @@
name: platform_body_parser
version: 5.3.0
description: Parse request bodies and query strings in Dart. Supports JSON, URL-encoded, and multi-part bodies.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/body_parser
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
http_parser: ^4.0.0
platform_http_server: ^4.4.0
mime: ^2.0.0
dev_dependencies:
http: ^1.0.0
test: ^1.24.0
lints: ^4.0.0

View file

@ -0,0 +1,154 @@
import 'dart:io';
import 'dart:convert';
import 'package:platform_body_parser/body_parser.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:test/test.dart';
import 'server_test.dart';
Future<BodyParseResult> _parseBody(HttpRequest request) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: false);
}
void main() {
HttpServer? server;
String? url;
http.Client? client;
setUp(() async {
server = await HttpServer.bind('127.0.0.1', 0);
server!.listen((HttpRequest request) async {
//Server will simply return a JSON representation of the parsed body
// ignore: deprecated_member_use
request.response.write(jsonEncodeBody(await _parseBody(request)));
await request.response.close();
});
url = 'http://localhost:${server!.port}';
print('Test server listening on $url');
client = http.Client();
});
tearDown(() async {
await server!.close(force: true);
client!.close();
server = null;
url = null;
client = null;
});
test('No upload', () async {
var boundary = 'myBoundary';
var headers = <String, String>{
'content-type': 'multipart/form-data; boundary=$boundary'
};
var postData = '''
--$boundary
Content-Disposition: form-data; name="hello"
world
--$boundary--
'''
.replaceAll('\n', '\r\n');
print(
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var jsons = json.decode(response.body);
var files = jsons['files'].map((map) {
return map.keys.fold<Map<String, dynamic>>(
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
});
expect(files.length, equals(0));
expect(jsons['body']['hello'], equals('world'));
});
test('Single upload', () async {
var boundary = 'myBoundary';
var headers = <String, String>{
'content-type': ContentType('multipart', 'form-data',
parameters: {'boundary': boundary}).toString()
};
var postData = '''
--$boundary
Content-Disposition: form-data; name="hello"
world
--$boundary
Content-Disposition: form-data; name="file"; filename="app.dart"
Content-Type: application/dart
Hello world
--$boundary--
'''
.replaceAll('\n', '\r\n');
print(
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var jsons = json.decode(response.body);
var files = jsons['files'];
expect(files.length, equals(1));
expect(files[0]['name'], equals('file'));
expect(files[0]['mimeType'], equals('application/dart'));
expect(files[0]['data'].length, equals(11));
expect(files[0]['filename'], equals('app.dart'));
expect(jsons['body']['hello'], equals('world'));
});
test('Multiple upload', () async {
var boundary = 'myBoundary';
var headers = <String, String>{
'content-type': 'multipart/form-data; boundary=$boundary'
};
var postData = '''
--$boundary
Content-Disposition: form-data; name="json"
god
--$boundary
Content-Disposition: form-data; name="num"
14.50000
--$boundary
Content-Disposition: form-data; name="file"; filename="app.dart"
Content-Type: text/plain
Hello world
--$boundary
Content-Disposition: form-data; name="entry-point"; filename="main.js"
Content-Type: text/javascript
function main() {
console.log("Hello, world!");
}
--$boundary--
'''
.replaceAll('\n', '\r\n');
print(
'Form Data: \n${postData.replaceAll("\r", "\\r").replaceAll("\n", "\\n")}');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var jsons = json.decode(response.body);
var files = jsons['files'];
expect(files.length, equals(2));
expect(files[0]['name'], equals('file'));
expect(files[0]['mimeType'], equals('text/plain'));
expect(files[0]['data'].length, equals(11));
expect(files[1]['name'], equals('entry-point'));
expect(files[1]['mimeType'], equals('text/javascript'));
expect(jsons['body']['json'], equals('god'));
expect(jsons['body']['num'], equals(14.5));
});
}

View file

@ -0,0 +1,174 @@
import 'dart:convert';
import 'dart:io' show HttpRequest, HttpServer;
import 'package:platform_body_parser/body_parser.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'package:test/test.dart';
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjcuMC4wLjEiLCJleHAiOi0xLCJpYXQiOiIyMDE2LTEyLTIyVDEyOjQ5OjUwLjM2MTQ0NiIsImlzcyI6ImFuZ2VsX2F1dGgiLCJzdWIiOiIxMDY2OTQ4Mzk2MDIwMjg5ODM2NTYifQ==.PYw7yUb-cFWD7N0sSLztP7eeRvO44nu1J2OgDNyT060=';
String jsonEncodeBody(BodyParseResult result) {
return json.encode({
'query': result.query,
'body': result.body,
'error': result.error?.toString(),
'files': result.files.map((f) {
return {
'name': f.name,
'mimeType': f.mimeType,
'filename': f.filename,
'data': f.data,
};
}).toList(),
'originalBuffer': result.originalBuffer,
'stack': null, //result.stack.toString(),
});
}
Future<BodyParseResult> _parseBody(HttpRequest request) {
return parseBodyFromStream(
request,
request.headers.contentType != null
? MediaType.parse(request.headers.contentType.toString())
: null,
request.uri,
storeOriginalBuffer: true);
}
void main() {
HttpServer? server;
String? url;
http.Client? client;
setUp(() async {
server = await HttpServer.bind('127.0.0.1', 0);
server!.listen((HttpRequest request) async {
//Server will simply return a JSON representation of the parsed body
request.response.write(
// ignore: deprecated_member_use
jsonEncodeBody(await _parseBody(request)));
await request.response.close();
});
url = 'http://localhost:${server!.port}';
print('Test server listening on $url');
client = http.Client();
});
tearDown(() async {
await server!.close(force: true);
client!.close();
server = null;
url = null;
client = null;
});
group('query string', () {
test('GET Simple', () async {
print('GET $url/?hello=world');
var response = await client!.get(Uri.parse('$url/?hello=world'));
print('Response: ${response.body}');
var result = json.decode(response.body);
expect(result['body'], equals({}));
expect(result['query'], equals({'hello': 'world'}));
expect(result['files'], equals([]));
//expect(result['originalBuffer'], isNull);
});
test('GET Complex', () async {
var postData =
'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - 1}&map.foo.bar=baz';
print('Body: $postData');
var response = await client!.get(Uri.parse('$url/?$postData'));
print('Response: ${response.body}');
var query = json.decode(response.body)['query'];
expect(query['hello'], equals('world'));
expect(query['nums'][2], equals(2));
expect(query['map'] is Map, equals(true));
expect(query['map']['foo'], equals({'bar': 'baz'}));
});
test('JWT', () async {
var postData = 'token=$token';
print('Body: $postData');
var response = await client!.get(Uri.parse('$url/?$postData'));
print('Response: ${response.body}');
var query = json.decode(response.body)['query'];
expect(query['token'], equals(token));
});
});
group('urlencoded', () {
var headers = <String, String>{
'content-type': 'application/x-www-form-urlencoded'
};
test('POST Simple', () async {
print('Body: hello=world');
var response = await client!
.post(Uri.parse(url!), headers: headers, body: 'hello=world');
print('Response: ${response.body}');
var result = json.decode(response.body);
expect(result['query'], equals({}));
expect(result['body'], equals({'hello': 'world'}));
expect(result['files'], equals([]));
expect(result['originalBuffer'], isList);
expect(result['originalBuffer'], isNotEmpty);
});
test('Post Complex', () async {
var postData =
'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 - 1}&map.foo.bar=baz';
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var body = json.decode(response.body)['body'];
expect(body['hello'], equals('world'));
expect(body['nums'][2], equals(2));
expect(body['map'] is Map, equals(true));
expect(body['map']['foo'], equals({'bar': 'baz'}));
});
test('JWT', () async {
var postData = 'token=$token';
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
var body = json.decode(response.body)['body'];
expect(body['token'], equals(token));
});
});
group('json', () {
var headers = <String, String>{'content-type': 'application/json'};
test('Post Simple', () async {
var postData = json.encode({'hello': 'world'});
print('Body: $postData');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var result = json.decode(response.body);
expect(result['body'], equals({'hello': 'world'}));
expect(result['query'], equals({}));
expect(result['files'], equals([]));
expect(result['originalBuffer'], allOf(isList, isNotEmpty));
});
test('Post Complex', () async {
var postData = json.encode({
'hello': 'world',
'nums': [1, 2.0, 3 - 1],
'map': {
'foo': {'bar': 'baz'}
}
});
print('Body: $postData');
var response =
await client!.post(Uri.parse(url!), headers: headers, body: postData);
print('Response: ${response.body}');
var body = json.decode(response.body)['body'];
expect(body['hello'], equals('world'));
expect(body['nums'][2], equals(2));
expect(body['map'] is Map, equals(true));
expect(body['map']['foo'], equals({'bar': 'baz'}));
});
});
}

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,55 @@
# Change Log
## 5.2.0
* Require Dart >= 3.3
* Updated `lints` to 4.0.0
## 5.1.0
* Updated `lints` to 3.0.0
## 5.0.0
* Require Dart >= 3.0
## 5.0.0-beta.1
* Require Dart >= 3.0
## 4.0.0
* Require Dart >= 2.17
## 3.0.2
* Fixed license link
## 3.0.1
* Updated README
## 3.0.0
* Upgraded from `pendantic` to `lints` linter
* Published as `platform_code_buffer` package
## 2.0.3
* Resolved static analysis warnings
## 2.0.2
* Updated README
## 2.0.1
* Fixed invalid homepage url in pubspec.yaml
## 2.0.0
* Migrated to support Dart SDK 2.12.x NNBD
## 1.0.1
* Added `CodeBuffer.noWhitespace()`.

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,69 @@
# Belatuk Code Buffer
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/platform_code_buffer?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/blob/main/packages/code_buffer/LICENSE)
**Replacement of `package:code_buffer` with breaking changes to support NNBD.**
An advanced StringBuffer geared toward generating code, and source maps.
## Installation
In your `pubspec.yaml`:
```yaml
dependencies:
platform_code_buffer: ^5.1.0
```
## Usage
Use a `CodeBuffer` just like any regular `StringBuffer`:
```dart
String someFunc() {
var buf = CodeBuffer();
buf
..write('hello ')
..writeln('world!');
return buf.toString();
}
```
However, a `CodeBuffer` supports indentation.
```dart
void someOtherFunc() {
var buf = CodeBuffer();
// Custom options...
var buf = CodeBuffer(newline: '\r\n', space: '\t', trailingNewline: true);
// Any following lines will have an incremented indentation level...
buf.indent();
// And vice-versa:
buf.outdent();
}
```
`CodeBuffer` instances keep track of every `SourceSpan` they create.
This makes them useful for codegen tools, or to-JS compilers.
```dart
void someFunc(CodeBuffer buf) {
buf.write('hello');
expect(buf.lastLine.text, 'hello');
buf.writeln('world');
expect(buf.lastLine.lastSpan.start.column, 5);
}
```
You can copy a `CodeBuffer` into another, heeding indentation rules:
```dart
void yetAnotherFunc(CodeBuffer a, CodeBuffer b) {
b.copyInto(a);
}
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,46 @@
import 'package:platform_code_buffer/code_buffer.dart';
import 'package:test/test.dart';
/// Use a `CodeBuffer` just like any regular `StringBuffer`:
String someFunc() {
var buf = CodeBuffer();
buf
..write('hello ')
..writeln('world!');
return buf.toString();
}
/// However, a `CodeBuffer` supports indentation.
void someOtherFunc() {
var buf = CodeBuffer();
// Custom options...
// ignore: unused_local_variable
var customBuf =
CodeBuffer(newline: '\r\n', space: '\t', trailingNewline: true);
// Without whitespace..
// ignore: unused_local_variable
var minifyingBuf = CodeBuffer.noWhitespace();
// Any following lines will have an incremented indentation level...
buf.indent();
// And vice-versa:
buf.outdent();
}
/// `CodeBuffer` instances keep track of every `SourceSpan` they create.
//This makes them useful for codegen tools, or to-JS compilers.
void yetAnotherOtherFunc(CodeBuffer buf) {
buf.write('hello');
expect(buf.lastLine!.text, 'hello');
buf.writeln('world');
expect(buf.lastLine!.lastSpan!.start.column, 5);
}
/// You can copy a `CodeBuffer` into another, heeding indentation rules:
void yetEvenAnotherFunc(CodeBuffer a, CodeBuffer b) {
b.copyInto(a);
}

View file

@ -0,0 +1,231 @@
import 'package:source_span/source_span.dart';
/// An advanced StringBuffer geared toward generating code, and source maps.
class CodeBuffer implements StringBuffer {
/// The character sequence used to represent a line break.
final String newline;
/// The character sequence used to represent a space/tab.
final String space;
/// The source URL to be applied to all generated [SourceSpan] instances.
final dynamic sourceUrl;
/// If `true` (default: `false`), then an additional [newline] will be inserted at the end of the generated string.
final bool trailingNewline;
final List<CodeBufferLine> _lines = [];
CodeBufferLine? _currentLine, _lastLine;
int _indentationLevel = 0;
int _length = 0;
CodeBuffer(
{this.space = ' ',
this.newline = '\n',
this.trailingNewline = false,
this.sourceUrl});
/// Creates a [CodeBuffer] that does not emit additional whitespace.
factory CodeBuffer.noWhitespace({sourceUrl}) => CodeBuffer(
space: '', newline: '', trailingNewline: false, sourceUrl: sourceUrl);
/// The last line created within this buffer.
CodeBufferLine? get lastLine => _lastLine;
/// Returns an immutable collection of the [CodeBufferLine]s within this instance.
List<CodeBufferLine> get lines => List<CodeBufferLine>.unmodifiable(_lines);
@override
bool get isEmpty => _lines.isEmpty;
@override
bool get isNotEmpty => _lines.isNotEmpty;
@override
int get length => _length;
CodeBufferLine _createLine() {
var start = SourceLocation(
_length,
sourceUrl: sourceUrl,
line: _lines.length,
column: _indentationLevel * space.length,
);
var line = CodeBufferLine._(_indentationLevel, start).._end = start;
_lines.add(_lastLine = line);
return line;
}
/// Increments the indentation level.
void indent() {
_indentationLevel++;
}
/// Decrements the indentation level, if it is greater than `0`.
void outdent() {
if (_indentationLevel > 0) _indentationLevel--;
}
/// Copies the contents of this [CodeBuffer] into another, preserving indentation and source mapping information.
void copyInto(CodeBuffer other) {
if (_lines.isEmpty) return;
var i = 0;
for (var line in _lines) {
// To compute offset:
// 1. Find current length of other
// 2. Add length of its newline
// 3. Add indentation
var column = (other._indentationLevel + line.indentationLevel) *
other.space.length;
var offset = other._length + other.newline.length + column;
// Re-compute start + end
var start = SourceLocation(
offset,
sourceUrl: other.sourceUrl,
line: other._lines.length + i,
column: column,
);
var end = SourceLocation(
offset + line.span.length,
sourceUrl: other.sourceUrl,
line: start.line,
column: column + line._buf.length,
);
var clone = CodeBufferLine._(
line.indentationLevel + other._indentationLevel, start)
.._end = end
.._buf.write(line._buf.toString());
// Adjust lastSpan
if (line._lastSpan != null) {
var s = line._lastSpan!.start;
var lastSpanColumn =
((line.indentationLevel + other._indentationLevel) *
other.space.length) +
line.text.indexOf(line._lastSpan!.text);
clone._lastSpan = SourceSpan(
SourceLocation(
offset + s.offset,
sourceUrl: other.sourceUrl,
line: clone.span.start.line,
column: lastSpanColumn,
),
SourceLocation(
offset + s.offset + line._lastSpan!.length,
sourceUrl: other.sourceUrl,
line: clone.span.end.line,
column: lastSpanColumn + line._lastSpan!.length,
),
line._lastSpan!.text,
);
}
other._lines.add(other._currentLine = other._lastLine = clone);
// Adjust length accordingly...
other._length = offset + clone.span.length;
i++;
}
other.writeln();
}
@override
void clear() {
_lines.clear();
_length = _indentationLevel = 0;
_currentLine = null;
}
@override
void writeCharCode(int charCode) {
_currentLine ??= _createLine();
_currentLine!._buf.writeCharCode(charCode);
var end = _currentLine!._end;
_currentLine!._end = SourceLocation(
end.offset + 1,
sourceUrl: end.sourceUrl,
line: end.line,
column: end.column + 1,
);
_length++;
_currentLine!._lastSpan =
SourceSpan(end, _currentLine!._end, String.fromCharCode(charCode));
}
@override
void write(Object? obj) {
var msg = obj.toString();
_currentLine ??= _createLine();
_currentLine!._buf.write(msg);
var end = _currentLine!._end;
_currentLine!._end = SourceLocation(
end.offset + msg.length,
sourceUrl: end.sourceUrl,
line: end.line,
column: end.column + msg.length,
);
_length += msg.length;
_currentLine!._lastSpan = SourceSpan(end, _currentLine!._end, msg);
}
@override
void writeln([Object? obj = '']) {
if (obj != null && obj != '') write(obj);
_currentLine = null;
_length++;
}
@override
void writeAll(Iterable objects, [String separator = '']) {
write(objects.join(separator));
}
@override
String toString() {
var buf = StringBuffer();
var i = 0;
for (var line in lines) {
if (i++ > 0) buf.write(newline);
for (var j = 0; j < line.indentationLevel; j++) {
buf.write(space);
}
buf.write(line._buf.toString());
}
if (trailingNewline == true) buf.write(newline);
return buf.toString();
}
}
/// Represents a line of text within a [CodeBuffer].
class CodeBufferLine {
/// Mappings from one [SourceSpan] to another, to aid with generating dynamic source maps.
final Map<SourceSpan, SourceSpan> sourceMappings = {};
/// The level of indentation preceding this line.
final int indentationLevel;
final SourceLocation _start;
final StringBuffer _buf = StringBuffer();
late SourceLocation _end;
SourceSpan? _lastSpan;
CodeBufferLine._(this.indentationLevel, this._start);
/// The [SourceSpan] corresponding to the last text written to this line.
SourceSpan? get lastSpan => _lastSpan;
/// The [SourceSpan] corresponding to this entire line.
SourceSpan get span => SourceSpan(_start, _end, _buf.toString());
/// The text within this line.
String get text => _buf.toString();
}

View file

@ -0,0 +1,410 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
charcode:
dependency: "direct main"
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: "direct main"
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test:
dependency: "direct dev"
description:
name: test
sha256: "22eb7769bee38c7e032d532e8daa2e1cc901b799f603550a4db8f3a5f5173ea2"
url: "https://pub.dev"
source: hosted
version: "1.25.12"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"

View file

@ -0,0 +1,12 @@
name: platform_code_buffer
version: 5.2.0
description: An advanced StringBuffer geared toward generating code, and source maps.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/code_buffer
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
charcode: ^1.2.0
source_span: ^1.8.1
dev_dependencies:
test: ^1.24.0
lints: ^4.0.0

View file

@ -0,0 +1,47 @@
import 'package:platform_code_buffer/code_buffer.dart';
import 'package:test/test.dart';
void main() {
var a = CodeBuffer(), b = CodeBuffer();
setUp(() {
a.writeln('outer block 1');
b
..writeln('inner block 1')
..writeln('inner block 2');
b.copyInto(a..indent());
a
..outdent()
..writeln('outer block 2');
});
tearDown(() {
a.clear();
b.clear();
});
test('sets correct text', () {
expect(
a.toString(),
[
'outer block 1',
' inner block 1',
' inner block 2',
'outer block 2',
].join('\n'));
});
test('sets lastLine+lastSpan', () {
var c = CodeBuffer()
..indent()
..write('>')
..writeln('innermost');
c.copyInto(a);
expect(a.lastLine!.text, '>innermost');
expect(a.lastLine!.span.start.column, 2);
expect(a.lastLine!.lastSpan!.start.line, 4);
expect(a.lastLine!.lastSpan!.start.column, 3);
expect(a.lastLine!.lastSpan!.end.line, 4);
expect(a.lastLine!.lastSpan!.end.column, 12);
});
}

View file

@ -0,0 +1,46 @@
import 'package:charcode/charcode.dart';
import 'package:platform_code_buffer/code_buffer.dart';
import 'package:test/test.dart';
void main() {
var buf = CodeBuffer();
tearDown(buf.clear);
test('writeCharCode', () {
buf.writeCharCode($x);
expect(buf.lastLine!.lastSpan!.start.column, 0);
expect(buf.lastLine!.lastSpan!.start.line, 0);
expect(buf.lastLine!.lastSpan!.end.column, 1);
expect(buf.lastLine!.lastSpan!.end.line, 0);
});
test('write', () {
buf.write('foo');
expect(buf.lastLine!.lastSpan!.start.column, 0);
expect(buf.lastLine!.lastSpan!.start.line, 0);
expect(buf.lastLine!.lastSpan!.end.column, 3);
expect(buf.lastLine!.lastSpan!.end.line, 0);
});
test('multiple writes in one line', () {
buf
..write('foo')
..write('baz');
expect(buf.lastLine!.lastSpan!.start.column, 3);
expect(buf.lastLine!.lastSpan!.start.line, 0);
expect(buf.lastLine!.lastSpan!.end.column, 6);
expect(buf.lastLine!.lastSpan!.end.line, 0);
});
test('multiple lines', () {
buf
..writeln('foo')
..write('bar')
..write('+')
..writeln('baz');
expect(buf.lastLine!.lastSpan!.start.column, 4);
expect(buf.lastLine!.lastSpan!.start.line, 1);
expect(buf.lastLine!.lastSpan!.end.column, 7);
expect(buf.lastLine!.lastSpan!.end.line, 1);
});
}

View file

@ -0,0 +1,90 @@
import 'package:charcode/charcode.dart';
import 'package:test/test.dart';
import 'package:platform_code_buffer/code_buffer.dart';
void main() {
var buf = CodeBuffer();
tearDown(buf.clear);
test('writeCharCode', () {
buf.writeCharCode($x);
expect(buf.toString(), 'x');
});
test('write', () {
buf.write('hello world');
expect(buf.toString(), 'hello world');
});
test('custom space', () {
var b = CodeBuffer(space: '+')
..writeln('foo')
..indent()
..writeln('baz');
expect(b.toString(), 'foo\n+baz');
});
test('custom newline', () {
var b = CodeBuffer(newline: 'N')
..writeln('foo')
..indent()
..writeln('baz');
expect(b.toString(), 'fooN baz');
});
test('trailing newline', () {
var b = CodeBuffer(trailingNewline: true)..writeln('foo');
expect(b.toString(), 'foo\n');
});
group('multiple lines', () {
setUp(() {
buf
..writeln('foo')
..writeln('bar')
..writeln('baz');
expect(buf.lines, hasLength(3));
expect(buf.lines[0].text, 'foo');
expect(buf.lines[1].text, 'bar');
expect(buf.lines[2].text, 'baz');
});
});
test('indent', () {
buf
..writeln('foo')
..indent()
..writeln('bar')
..indent()
..writeln('baz')
..outdent()
..writeln('quux')
..outdent()
..writeln('end');
expect(buf.toString(), 'foo\n bar\n baz\n quux\nend');
});
group('sets lastLine text', () {
test('writeCharCode', () {
buf.writeCharCode($x);
expect(buf.lastLine!.text, 'x');
});
test('write', () {
buf.write('hello world');
expect(buf.lastLine!.text, 'hello world');
});
});
group('sets lastLine lastSpan', () {
test('writeCharCode', () {
buf.writeCharCode($x);
expect(buf.lastLine!.lastSpan!.text, 'x');
});
test('write', () {
buf.write('hello world');
expect(buf.lastLine!.lastSpan!.text, 'hello world');
});
});
}

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,61 @@
# Change Log
## 5.2.0
* Require Dart >= 3.3
* Updated `lints` to 4.0.0
## 5.1.0
* Updated `lints` to 3.0.0
* Fixed lints warnings
## 5.0.0
* Require Dart >= 3.0
## 5.0.0-beta.1
* Require Dart >= 3.0
* Updated `platform_code_buffer` to 5.0.0
## 4.0.0
* Require Dart >= 2.17
## 3.0.1
* Fixed license link
## 3.0.0
* Upgraded from `pendantic` to `lints` linter
* Published as `platform_combinator` package
* Resolved static analysis warnings
## 2.0.2
* Resolved static analysis warnings
## 2.0.1
* Updated README
## 2.0.0
* Migrated to support Dart SDK 2.12.x NNBD
## 1.1.0
* Add `tupleX` parsers. Hooray for strong typing!
## 1.0.0+3
* `then` now *always* returns `dynamic`.
## 1.0.0+2
* `star` now includes with a call to `opt`.
* Added comments.
* Enforce generics on `separatedBy`.
* Enforce Dart 2 semantics.

29
common/combinator/LICENSE Normal file
View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

129
common/combinator/README.md Normal file
View file

@ -0,0 +1,129 @@
# Belatuk Combinator
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/platform_combinator?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/blob/main/packages/combinator/LICENSE)
**Replacement of `package:combinator` with breaking changes to support NNBD.**
Packrat parser combinators that support static typing, generics, file spans, memoization, and more.
**RECOMMENDED:**
Check `example/` for examples. The examples contain examples of using:
* Generic typing
* Reading `FileSpan` from `ParseResult`
* More...
## Basic Usage
```dart
void main() {
// Parse a Pattern (usually String or RegExp).
var foo = match('foo');
var number = match(RegExp(r'[0-9]+'), errorMessage: 'Expected a number.');
// Set a value.
var numWithValue = number.map((r) => int.parse(r.span.text));
// Expect a pattern, or nothing.
var optional = numWithValue.opt();
// Expect a pattern zero or more times.
var star = optional.star();
// Expect one or more times.
var plus = optional.plus();
// Expect an arbitrary number of times.
var threeTimes = optional.times(3);
// Expect a sequence of patterns.
var doraTheExplorer = chain([
match('Dora').space(),
match('the').space(),
match('Explorer').space(),
]);
// Choose exactly one of a set of patterns, whichever
// appears first.
var alt = any([
match('1'),
match('11'),
match('111'),
]);
// Choose the *longest* match for any of the given alternatives.
var alt2 = longest([
match('1'),
match('11'),
match('111'),
]);
// Friendly operators
var fooOrNumber = foo | number;
var fooAndNumber = foo & number;
var notFoo = ~foo;
}
```
## Error Messages
Parsers without descriptive error messages can lead to frustrating dead-ends
for end-users. Fortunately, `platform_combinator` is built with error handling in mind.
```dart
void main(Parser parser) {
// Append an arbitrary error message to a parser if it is not matched.
var withError = parser.error(errorMessage: 'Hey!!! Wrong!!!');
// You can also set the severity of an error.
var asHint = parser.error(severity: SyntaxErrorSeverity.hint);
// Constructs like `any`, `chain`, and `longest` support this as well.
var foo = longest([
parser.error(errorMessage: 'foo'),
parser.error(errorMessage: 'bar')
], errorMessage: 'Expected a "foo" or a "bar"');
// If multiple errors are present at one location,
// it can create a lot of noise.
//
// Use `foldErrors` to only take one error at a given location.
var lessNoise = parser.foldErrors();
}
```
## Whitespaces
Handling optional whitespace is dead-easy:
```dart
void main(Parser parser) {
var optionalSpace = parser.space();
}
```
## For Programming Languages
`platform_combinator` was conceived to make writing parsers for complex grammars easier,
namely programming languages. Thus, there are functions built-in to make common constructs
easier:
```dart
void main(Parser parser) {
var array = parser
.separatedByComma()
.surroundedBySquareBrackets(defaultValue: []);
var braces = parser.surroundedByCurlyBraces();
var sep = parser.separatedBy(match('!').space());
}
```
## Differences between this and Petitparser
* `platform_combinator` makes extensive use of Dart's dynamic typing
* `platform_combinator` supports detailed error messages (with configurable severity)
* `platform_combinator` keeps track of locations (ex. `line 1: 3`)

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,56 @@
// Run this with "Basic QWxhZGRpbjpPcGVuU2VzYW1l"
import 'dart:convert';
import 'dart:io';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
/// Parse a part of a decoded Basic auth string.
///
/// Namely, the `username` or `password` in `{username}:{password}`.
final Parser<String> string =
match<String>(RegExp(r'[^:$]+'), errorMessage: 'Expected a string.')
.value((r) => r.span!.text);
/// Transforms `{username}:{password}` to `{"username": username, "password": password}`.
final Parser<Map<String, String>> credentials = chain<String>([
string.opt(),
match<String>(':'),
string.opt(),
]).map<Map<String, String>>(
(r) => {'username': r.value![0], 'password': r.value![2]});
/// We can actually embed a parser within another parser.
///
/// This is used here to BASE64URL-decode a string, and then
/// parse the decoded string.
final Parser credentialString = match<Map<String, String>?>(
RegExp(r'([^\n$]+)'),
errorMessage: 'Expected a credential string.')
.value((r) {
var decoded = utf8.decode(base64Url.decode(r.span!.text));
var scanner = SpanScanner(decoded);
return credentials.parse(scanner).value;
});
final Parser basic = match<void>('Basic').space();
final Parser basicAuth = basic.then(credentialString).index(1);
void main() {
while (true) {
stdout.write('Enter a basic auth value: ');
var line = stdin.readLineSync()!;
var scanner = SpanScanner(line, sourceUrl: 'stdin');
var result = basicAuth.parse(scanner);
if (!result.successful) {
for (var error in result.errors) {
print(error.toolString);
print(error.span!.highlight(color: true));
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,71 @@
import 'dart:math';
import 'dart:io';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
/// Note: This grammar does not handle precedence, for the sake of simplicity.
Parser<num> calculatorGrammar() {
var expr = reference<num>();
var number = match<num>(RegExp(r'-?[0-9]+(\.[0-9]+)?'))
.value((r) => num.parse(r.span!.text));
var hex = match<int>(RegExp(r'0x([A-Fa-f0-9]+)'))
.map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 16));
var binary = match<int>(RegExp(r'([0-1]+)b'))
.map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 2));
var alternatives = <Parser<num>>[];
void registerBinary(String op, num Function(num, num) f) {
alternatives.add(
chain<num>([
expr.space(),
match<void>(op).space() as Parser<num>,
expr.space(),
]).map((r) => f(r.value![0], r.value![2])),
);
}
registerBinary('**', (a, b) => pow(a, b));
registerBinary('*', (a, b) => a * b);
registerBinary('/', (a, b) => a / b);
registerBinary('%', (a, b) => a % b);
registerBinary('+', (a, b) => a + b);
registerBinary('-', (a, b) => a - b);
registerBinary('^', (a, b) => a.toInt() ^ b.toInt());
registerBinary('&', (a, b) => a.toInt() & b.toInt());
registerBinary('|', (a, b) => a.toInt() | b.toInt());
alternatives.addAll([
number,
hex,
binary,
expr.parenthesized(),
]);
expr.parser = longest(alternatives);
return expr;
}
void main() {
var calculator = calculatorGrammar();
while (true) {
stdout.write('Enter an expression: ');
var line = stdin.readLineSync()!;
var scanner = SpanScanner(line, sourceUrl: 'stdin');
var result = calculator.parse(scanner);
if (!result.successful) {
for (var error in result.errors) {
stderr.writeln(error.toolString);
stderr.writeln(error.span!.highlight(color: true));
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,29 @@
import 'dart:io';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
final Parser<String> id =
match<String>(RegExp(r'[A-Za-z]+')).value((r) => r.span!.text);
// We can use `separatedBy` to easily construct parser
// that can be matched multiple times, separated by another
// pattern.
//
// This is useful for parsing arrays or map literals.
void main() {
while (true) {
stdout.write('Enter a string (ex "a,b,c"): ');
var line = stdin.readLineSync()!;
var scanner = SpanScanner(line, sourceUrl: 'stdin');
var result = id.separatedBy(match(',').space()).parse(scanner);
if (!result.successful) {
for (var error in result.errors) {
print(error.toolString);
print(error.span!.highlight(color: true));
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,71 @@
import 'dart:io';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
Parser jsonGrammar() {
var expr = reference();
// Parse a number
var number = match<num>(RegExp(r'-?[0-9]+(\.[0-9]+)?'),
errorMessage: 'Expected a number.')
.value(
(r) => num.parse(r.span!.text),
);
// Parse a string (no escapes supported, because lazy).
var string =
match(RegExp(r'"[^"]*"'), errorMessage: 'Expected a string.').value(
(r) => r.span!.text.substring(1, r.span!.text.length - 1),
);
// Parse an array
var array = expr
.space()
.separatedByComma()
.surroundedBySquareBrackets(defaultValue: []);
// KV pair
var keyValuePair = chain([
string.space(),
match(':').space(),
expr.error(errorMessage: 'Missing expression.'),
]).castDynamic().cast<Map>().value((r) => {r.value![0]: r.value![2]});
// Parse an object.
var object = keyValuePair
.separatedByComma()
.castDynamic()
.surroundedByCurlyBraces(defaultValue: {});
expr.parser = longest(
[
array,
number,
string,
object.error(),
],
errorMessage: 'Expected an expression.',
).space();
return expr.foldErrors();
}
void main() {
var json = jsonGrammar();
while (true) {
stdout.write('Enter some JSON: ');
var line = stdin.readLineSync()!;
var scanner = SpanScanner(line, sourceUrl: 'stdin');
var result = json.parse(scanner);
if (!result.successful) {
for (var error in result.errors) {
print(error.toolString);
print(error.span!.highlight(color: true));
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,38 @@
import 'dart:io';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
final Parser minus = match('-');
final Parser<int> digit =
match(RegExp(r'[0-9]'), errorMessage: 'Expected a number');
final Parser digits = digit.plus();
final Parser dot = match('.');
final Parser decimal = ( // digits, (dot, digits)?
digits & (dot & digits).opt() //
);
final Parser number = //
(minus.opt() & decimal) // minus?, decimal
.map<num>((r) => num.parse(r.span!.text));
void main() {
while (true) {
stdout.write('Enter a number: ');
var line = stdin.readLineSync()!;
var scanner = SpanScanner(line, sourceUrl: 'stdin');
var result = number.parse(scanner);
if (!result.successful) {
for (var error in result.errors) {
stderr.writeln(error.toolString);
stderr.writeln(error.span!.highlight(color: true));
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,45 @@
// For some reason, this cannot be run in checked mode???
import 'dart:io';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
final Parser<String> key =
match<String>(RegExp(r'[^=&\n]+'), errorMessage: 'Missing k/v')
.value((r) => r.span!.text);
final Parser value = key.map((r) => Uri.decodeQueryComponent(r.value!));
final Parser pair = chain([
key,
match('='),
value,
]).map((r) {
return {
r.value![0]: r.value![2],
};
});
final Parser pairs = pair
.separatedBy(match(r'&'))
.map((r) => r.value!.reduce((a, b) => a..addAll(b)));
final Parser queryString = pairs.opt();
void main() {
while (true) {
stdout.write('Enter a query string: ');
var line = stdin.readLineSync()!;
var scanner = SpanScanner(line, sourceUrl: 'stdin');
var result = pairs.parse(scanner);
if (!result.successful) {
for (var error in result.errors) {
print(error.toolString);
print(error.span!.highlight(color: true));
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,85 @@
import 'dart:collection';
import 'dart:io';
import 'dart:math';
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
import 'package:tuple/tuple.dart';
void main() {
var expr = reference();
var symbols = <String, dynamic>{};
void registerFunction(String name, int nArgs, Function(List<num>) f) {
symbols[name] = Tuple2(nArgs, f);
}
registerFunction('**', 2, (args) => pow(args[0], args[1]));
registerFunction('*', 2, (args) => args[0] * args[1]);
registerFunction('/', 2, (args) => args[0] / args[1]);
registerFunction('%', 2, (args) => args[0] % args[1]);
registerFunction('+', 2, (args) => args[0] + args[1]);
registerFunction('-', 2, (args) => args[0] - args[1]);
registerFunction('.', 1, (args) => args[0].toDouble());
registerFunction('print', 1, (args) {
print(args[0]);
return args[0];
});
var number =
match(RegExp(r'[0-9]+(\.[0-9]+)?'), errorMessage: 'Expected a number.')
.map((r) => num.parse(r.span!.text));
var id = match(
RegExp(
r'[A-Za-z_!\\$",\\+-\\./:;\\?<>%&\\*@\[\]\\{\}\\|`\\^~][A-Za-z0-9_!\\$",\\+-\\./:;\\?<>%&\*@\[\]\\{\}\\|`\\^~]*'),
errorMessage: 'Expected an ID')
.map((r) => symbols[r.span!.text] ??=
throw "Undefined symbol: '${r.span!.text}'");
var atom = number.castDynamic().or(id);
var list = expr.space().times(2, exact: false).map((r) {
try {
var out = [];
var q = Queue.from(r.value!.reversed);
while (q.isNotEmpty) {
var current = q.removeFirst();
if (current is! Tuple2) {
out.insert(0, current);
} else {
var args = [];
for (var i = 0; i < (current.item1 as num); i++) {
args.add(out.removeLast());
}
out.add(current.item2(args));
}
}
return out.length == 1 ? out.first : out;
} catch (_) {
return [];
}
});
expr.parser = longest([
list,
atom,
expr.parenthesized(),
]); //list | atom | expr.parenthesized();
while (true) {
stdout.write('> ');
var line = stdin.readLineSync()!;
var result = expr.parse(SpanScanner(line));
if (result.errors.isNotEmpty) {
for (var error in result.errors) {
print(error.toolString);
print(error.message);
}
} else {
print(result.value);
}
}
}

View file

@ -0,0 +1,14 @@
import 'package:platform_combinator/combinator.dart';
import 'package:string_scanner/string_scanner.dart';
void main() {
var pub = match('pub').map((r) => r.span!.text).space();
var dart = match('dart').map((r) => 24).space();
var lang = match('lang').map((r) => true).space();
// Parses a Tuple3<String, int, bool>
var grammar = tuple3(pub, dart, lang);
var scanner = SpanScanner('pub dart lang');
print(grammar.parse(scanner).value);
}

View file

@ -0,0 +1,2 @@
export 'src/combinator/combinator.dart';
export 'src/error.dart';

View file

@ -0,0 +1,26 @@
part of 'combinator.dart';
class _Advance<T> extends Parser<T> {
final Parser<T> parser;
final int amount;
_Advance(this.parser, this.amount);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
if (result.successful) args.scanner.position += amount;
return result;
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('advance($amount) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,85 @@
part of 'combinator.dart';
/// Matches any one of the given [parsers].
///
/// If [backtrack] is `true` (default), a failed parse will not modify the scanner state.
///
/// You can provide a custom [errorMessage]. You can set it to `false` to not
/// generate any error at all.
Parser<T> any<T>(Iterable<Parser<T>> parsers,
{bool backtrack = true, errorMessage, SyntaxErrorSeverity? severity}) {
return _Any(parsers, backtrack != false, errorMessage,
severity ?? SyntaxErrorSeverity.error);
}
class _Any<T> extends Parser<T> {
final Iterable<Parser<T>> parsers;
final bool backtrack;
final dynamic errorMessage;
final SyntaxErrorSeverity severity;
_Any(this.parsers, this.backtrack, this.errorMessage, this.severity);
@override
ParseResult<T> _parse(ParseArgs args) {
var inactive = parsers
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
if (inactive.isEmpty) {
return ParseResult(args.trampoline, args.scanner, this, false, []);
}
var errors = <SyntaxError>[];
var replay = args.scanner.position;
for (var parser in inactive) {
var result = parser._parse(args.increaseDepth());
if (result.successful) {
return result;
} else {
if (backtrack) args.scanner.position = replay;
if (parser is _Alt) errors.addAll(result.errors);
}
}
if (errorMessage != false) {
errors.add(
SyntaxError(
severity,
errorMessage?.toString() ??
'No match found for ${parsers.length} alternative(s)',
args.scanner.emptySpan,
),
);
}
return ParseResult(args.trampoline, args.scanner, this, false, errors);
}
@override
ParseResult<T> __parse(ParseArgs args) {
// Never called
throw ArgumentError('[Combinator] Invalid method call');
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('any(${parsers.length}) (')
..indent();
var i = 1;
for (var parser in parsers) {
buffer
..writeln('#${i++}:')
..indent();
parser.stringify(buffer);
buffer.outdent();
}
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,26 @@
part of 'combinator.dart';
class _Cache<T> extends Parser<T> {
final Map<int, ParseResult<T>> _cache = {};
final Parser<T> parser;
_Cache(this.parser);
@override
ParseResult<T> __parse(ParseArgs args) {
return _cache.putIfAbsent(args.scanner.position, () {
return parser._parse(args.increaseDepth());
}).change(parser: this);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('cache(${_cache.length}) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,63 @@
part of 'combinator.dart';
class _Cast<T, U extends T> extends Parser<U> {
final Parser<T> parser;
_Cast(this.parser);
@override
ParseResult<U> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
return ParseResult<U>(
args.trampoline,
args.scanner,
this,
result.successful,
result.errors,
span: result.span,
value: result.value as U?,
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('cast<$U> (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}
class _CastDynamic<T> extends Parser<dynamic> {
final Parser<T> parser;
_CastDynamic(this.parser);
@override
ParseResult<dynamic> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
return ParseResult<dynamic>(
args.trampoline,
args.scanner,
this,
result.successful,
result.errors,
span: result.span,
value: result.value,
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('cast<dynamic> (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,111 @@
part of 'combinator.dart';
/// Expects to parse a sequence of [parsers].
///
/// If [failFast] is `true` (default), then the first failure to parse will abort the parse.
ListParser<T> chain<T>(Iterable<Parser<T>> parsers,
{bool failFast = true, SyntaxErrorSeverity? severity}) {
return _Chain<T>(
parsers, failFast != false, severity ?? SyntaxErrorSeverity.error);
}
class _Alt<T> extends Parser<T> {
final Parser<T> parser;
final String? errorMessage;
final SyntaxErrorSeverity severity;
_Alt(this.parser, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
return result.successful
? result
: result.addErrors([
SyntaxError(
severity, errorMessage, result.span ?? args.scanner.emptySpan),
]);
}
@override
void stringify(CodeBuffer buffer) {
parser.stringify(buffer);
}
}
class _Chain<T> extends ListParser<T> {
final Iterable<Parser<T>> parsers;
final bool failFast;
final SyntaxErrorSeverity severity;
_Chain(this.parsers, this.failFast, this.severity);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
var errors = <SyntaxError>[];
var results = <T>[];
var spans = <FileSpan>[];
var successful = true;
for (var parser in parsers) {
var result = parser._parse(args.increaseDepth());
if (!result.successful) {
if (parser is _Alt) errors.addAll(result.errors);
if (failFast) {
return ParseResult(
args.trampoline, args.scanner, this, false, result.errors);
}
successful = false;
}
if (result.value != null) {
results.add(result.value as T);
} else {
results.add('NULL' as T);
}
if (result.span != null) {
spans.add(result.span!);
}
}
FileSpan? span;
if (spans.isNotEmpty) {
span = spans.reduce((a, b) => a.expand(b));
}
return ParseResult<List<T>>(
args.trampoline,
args.scanner,
this,
successful,
errors,
span: span,
value: List<T>.unmodifiable(results),
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('chain(${parsers.length}) (')
..indent();
var i = 1;
for (var parser in parsers) {
buffer
..writeln('#${i++}:')
..indent();
parser.stringify(buffer);
buffer.outdent();
}
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,42 @@
part of 'combinator.dart';
class _Check<T> extends Parser<T> {
final Parser<T> parser;
final Matcher matcher;
final String? errorMessage;
final SyntaxErrorSeverity severity;
_Check(this.parser, this.matcher, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
var matchState = {};
var result = parser._parse(args.increaseDepth()).change(parser: this);
if (!result.successful) {
return result;
} else if (!matcher.matches(result.value, matchState)) {
return result.change(successful: false).addErrors([
SyntaxError(
severity,
errorMessage ??
'${matcher.describe(StringDescription('Expected '))}.',
result.span,
),
]);
} else {
return result;
}
}
@override
void stringify(CodeBuffer buffer) {
var d = matcher.describe(StringDescription());
buffer
..writeln('check($d) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,394 @@
library lex.src.combinator;
import 'dart:collection';
import 'package:platform_code_buffer/code_buffer.dart';
import 'package:matcher/matcher.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
import 'package:tuple/tuple.dart';
import '../error.dart';
part 'any.dart';
part 'advance.dart';
part 'cache.dart';
part 'cast.dart';
part 'chain.dart';
part 'check.dart';
part 'compare.dart';
part 'fold_errors.dart';
part 'index.dart';
part 'longest.dart';
part 'map.dart';
part 'match.dart';
part 'max_depth.dart';
part 'negate.dart';
part 'opt.dart';
part 'recursion.dart';
part 'reduce.dart';
part 'reference.dart';
part 'repeat.dart';
part 'safe.dart';
part 'to_list.dart';
part 'util.dart';
part 'value.dart';
class ParseArgs {
final Trampoline trampoline;
final SpanScanner scanner;
final int depth;
ParseArgs(this.trampoline, this.scanner, this.depth);
ParseArgs increaseDepth() => ParseArgs(trampoline, scanner, depth + 1);
}
/// A parser combinator, which can parse very complicated grammars in a manageable manner.
abstract class Parser<T> {
ParseResult<T> __parse(ParseArgs args);
ParseResult<T> _parse(ParseArgs args) {
var pos = args.scanner.position;
if (args.trampoline.hasMemoized(this, pos)) {
return args.trampoline.getMemoized<T>(this, pos);
}
if (args.trampoline.isActive(this, pos)) {
return ParseResult(args.trampoline, args.scanner, this, false, []);
}
args.trampoline.enter(this, pos);
var result = __parse(args);
args.trampoline.memoize(this, pos, result);
args.trampoline.exit(this);
return result;
}
/// Parses text from a [SpanScanner].
ParseResult<T> parse(SpanScanner scanner, [int depth = 1]) {
var args = ParseArgs(Trampoline(), scanner, depth);
return _parse(args);
}
/// Skips forward a certain amount of steps after parsing, if it was successful.
Parser<T> forward(int amount) => _Advance<T>(this, amount);
/// Moves backward a certain amount of steps after parsing, if it was successful.
Parser<T> back(int amount) => _Advance<T>(this, amount * -1);
/// Casts this parser to produce [U] objects.
Parser<U> cast<U extends T>() => _Cast<T, U>(this);
/// Casts this parser to produce [dynamic] objects.
Parser<dynamic> castDynamic() => _CastDynamic<T>(this);
/// Runs the given function, which changes the returned [ParseResult] into one relating to a [U] object.
Parser<U> change<U>(ParseResult<U> Function(ParseResult<T>) f) {
return _Change<T, U>(this, f);
}
/// Validates the parse result against a [Matcher].
///
/// You can provide a custom [errorMessage].
Parser<T> check(Matcher matcher,
{String? errorMessage, SyntaxErrorSeverity? severity}) =>
_Check<T>(
this, matcher, errorMessage, severity ?? SyntaxErrorSeverity.error);
/// Binds an [errorMessage] to a copy of this parser.
Parser<T> error({String? errorMessage, SyntaxErrorSeverity? severity}) =>
_Alt<T>(this, errorMessage, severity ?? SyntaxErrorSeverity.error);
/// Removes multiple errors that occur in the same spot; this can reduce noise in parser output.
Parser<T> foldErrors({bool Function(SyntaxError a, SyntaxError b)? equal}) {
equal ??= (b, e) => b.span?.start.offset == e.span?.start.offset;
return _FoldErrors<T>(this, equal);
}
/// Transforms the parse result using a unary function.
Parser<U> map<U>(U Function(ParseResult<T>) f) {
return _Map<T, U>(this, f);
}
/// Prevents recursion past a certain [depth], preventing stack overflow errors.
Parser<T> maxDepth(int depth) => _MaxDepth<T>(this, depth);
Parser<T> operator ~() => negate();
/// Ensures this pattern is not matched.
///
/// You can provide an [errorMessage].
Parser<T> negate(
{String errorMessage = 'Negate error',
SyntaxErrorSeverity severity = SyntaxErrorSeverity.error}) =>
_Negate<T>(this, errorMessage, severity);
/// Caches the results of parse attempts at various locations within the source text.
///
/// Use this to prevent excessive recursion.
Parser<T> cache() => _Cache<T>(this);
Parser<T> operator &(Parser<T> other) => and(other);
/// Consumes `this` and another parser, but only considers the result of `this` parser.
Parser<T> and(Parser other) => then(other).change<T>((r) {
return ParseResult<T>(
r.trampoline,
r.scanner,
this,
r.successful,
r.errors,
span: r.span,
value: (r.value != null ? r.value![0] : r.value) as T?,
);
});
Parser<T> operator |(Parser<T> other) => or(other);
/// Shortcut for [or]-ing two parsers.
Parser<T> or<U>(Parser<T> other) => any<T>([this, other]);
/// Parses this sequence one or more times.
ListParser<T> plus() => times(1, exact: false);
/// Safely escapes this parser when an error occurs.
///
/// The generated parser only runs once; repeated uses always exit eagerly.
Parser<T> safe(
{bool backtrack = true,
String errorMessage = 'error',
SyntaxErrorSeverity? severity}) =>
_Safe<T>(
this, backtrack, errorMessage, severity ?? SyntaxErrorSeverity.error);
Parser<List<T>> separatedByComma() =>
separatedBy(match<List<T>>(',').space());
/// Expects to see an infinite amounts of the pattern, separated by the [other] pattern.
///
/// Use this as a shortcut to parse arrays, parameter lists, etc.
Parser<List<T>> separatedBy(Parser other) {
var suffix = other.then(this).index(1).cast<T>();
return then(suffix.star()).map((r) {
var v = r.value;
if (v == null || v.length < 2) {
return [];
}
var preceding = v.isEmpty ? [] : (v[0] == null ? [] : [v[0]]);
var out = List<T>.from(preceding);
if (v[1] != null && v[1] != 'NULL') {
v[1].forEach((element) {
out.add(element as T);
});
}
return out;
});
}
Parser<T> surroundedByCurlyBraces({required T defaultValue}) => opt()
.surroundedBy(match('{').space(), match('}').space())
.map((r) => r.value ?? defaultValue);
Parser<T> surroundedBySquareBrackets({required T defaultValue}) => opt()
.surroundedBy(match('[').space(), match(']').space())
.map((r) => r.value ?? defaultValue);
/// Expects to see the pattern, surrounded by the others.
///
/// If no [right] is provided, it expects to see the same pattern on both sides.
/// Use this parse things like parenthesized expressions, arrays, etc.
Parser<T> surroundedBy(Parser left, [Parser? right]) {
return chain([
left,
this,
right ?? left,
]).index(1).castDynamic().cast<T>();
}
/// Parses `this`, either as-is or wrapped in parentheses.
Parser<T> maybeParenthesized() {
return any([parenthesized(), this]);
}
/// Parses `this`, wrapped in parentheses.
Parser<T> parenthesized() =>
surroundedBy(match('(').space(), match(')').space());
/// Consumes any trailing whitespace.
Parser<T> space() => trail(RegExp(r'[ \n\r\t]+'));
/// Consumes 0 or more instance(s) of this parser.
ListParser<T> star({bool backtrack = true}) =>
times(1, exact: false, backtrack: backtrack).opt();
/// Shortcut for [chain]-ing two parsers together.
ListParser<dynamic> then(Parser other) => chain<dynamic>([this, other]);
/// Casts this instance into a [ListParser].
ListParser<T> toList() => _ToList<T>(this);
/// Consumes and ignores any trailing occurrences of [pattern].
Parser<T> trail(Pattern pattern) =>
then(match(pattern).opt()).first().cast<T>();
/// Expect this pattern a certain number of times.
///
/// If [exact] is `false` (default: `true`), then the generated parser will accept
/// an infinite amount of occurrences after the specified [count].
///
/// You can provide custom error messages for when there are [tooFew] or [tooMany] occurrences.
ListParser<T> times(int count,
{bool exact = true,
String tooFew = 'Too few',
String tooMany = 'Too many',
bool backtrack = true,
SyntaxErrorSeverity? severity}) {
return _Repeat<T>(this, count, exact, tooFew, tooMany, backtrack,
severity ?? SyntaxErrorSeverity.error);
}
/// Produces an optional copy of this parser.
///
/// If [backtrack] is `true` (default), then a failed parse will not
/// modify the scanner state.
Parser<T> opt({bool backtrack = true}) => _Opt(this, backtrack);
/// Sets the value of the [ParseResult].
Parser<T> value(T Function(ParseResult<T?>) f) {
return _Value<T>(this, f);
}
/// Prints a representation of this parser, ideally without causing a stack overflow.
void stringify(CodeBuffer buffer);
}
/// A [Parser] that produces [List]s of a type [T].
abstract class ListParser<T> extends Parser<List<T>> {
/// Shortcut for calling [index] with `0`.
Parser<T> first() => index(0);
/// Modifies this parser to only return the value at the given index [i].
Parser<T> index(int i) => _Index<T>(this, i);
/// Shortcut for calling [index] with the greatest-possible index.
Parser<T> last() => index(-1);
/// Modifies this parser to call `List.reduce` on the parsed values.
Parser<T> reduce(T Function(T, T) combine) => _Reduce<T>(this, combine);
/// Sorts the parsed values, using the given [Comparator].
ListParser<T> sort(Comparator<T> compare) => _Compare(this, compare);
@override
ListParser<T> opt({bool backtrack = true}) => _ListOpt(this, backtrack);
/// Modifies this parser, returning only the values that match a predicate.
Parser<List<T>> where(bool Function(T) f) =>
map<List<T>>((r) => r.value?.where(f).toList() ?? []);
/// Condenses a [ListParser] into having a value of the combined span's text.
Parser<String> flatten() => map<String>((r) => r.span?.text ?? '');
}
/// Prevents stack overflow in recursive parsers.
class Trampoline {
final Map<Parser, Queue<int>> _active = {};
final Map<Parser, List<Tuple2<int, ParseResult>>> _memo = {};
bool hasMemoized(Parser parser, int position) {
var list = _memo[parser];
return list?.any((t) => t.item1 == position) == true;
}
ParseResult<T> getMemoized<T>(Parser parser, int position) {
return _memo[parser]?.firstWhere((t) => t.item1 == position).item2
as ParseResult<T>;
}
void memoize(Parser parser, int position, ParseResult? result) {
if (result != null) {
var list = _memo.putIfAbsent(parser, () => []);
var tuple = Tuple2(position, result);
if (!list.contains(tuple)) list.add(tuple);
}
}
bool isActive(Parser parser, int position) {
if (!_active.containsKey(parser)) {
return false;
}
var q = _active[parser]!;
if (q.isEmpty) return false;
//return q.contains(position);
return q.first == position;
}
void enter(Parser parser, int position) {
_active.putIfAbsent(parser, () => Queue()).addFirst(position);
}
void exit(Parser parser) {
if (_active.containsKey(parser)) _active[parser]?.removeFirst();
}
}
/// The result generated by a [Parser].
class ParseResult<T> {
final Parser<T> parser;
final bool successful;
final Iterable<SyntaxError> errors;
final FileSpan? span;
final T? value;
final SpanScanner scanner;
final Trampoline trampoline;
ParseResult(
this.trampoline, this.scanner, this.parser, this.successful, this.errors,
{this.span, this.value});
ParseResult<T> change(
{Parser<T>? parser,
bool? successful,
Iterable<SyntaxError> errors = const [],
FileSpan? span,
T? value}) {
return ParseResult<T>(
trampoline,
scanner,
parser ?? this.parser,
successful ?? this.successful,
errors.isNotEmpty ? errors : this.errors,
span: span ?? this.span,
value: value ?? this.value,
);
}
ParseResult<T> addErrors(Iterable<SyntaxError> errors) {
return change(
errors: List<SyntaxError>.from(this.errors)..addAll(errors),
);
}
}

View file

@ -0,0 +1,38 @@
part of 'combinator.dart';
class _Compare<T> extends ListParser<T> {
final ListParser<T> parser;
final Comparator<T> compare;
_Compare(this.parser, this.compare);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
if (!result.successful) return result;
result = result.change(
value: result.value?.isNotEmpty == true ? result.value : []);
result = result.change(value: List<T>.from(result.value!));
return ParseResult<List<T>>(
args.trampoline,
args.scanner,
this,
true,
[],
span: result.span,
value: result.value?..sort(compare),
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('sort($compare) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,29 @@
part of 'combinator.dart';
class _FoldErrors<T> extends Parser<T> {
final Parser<T> parser;
final bool Function(SyntaxError, SyntaxError) equal;
_FoldErrors(this.parser, this.equal);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
var errors = result.errors.fold<List<SyntaxError>>([], (out, e) {
if (!out.any((b) => equal(e, b))) out.add(e);
return out;
});
return result.change(errors: errors);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('fold errors (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,52 @@
part of 'combinator.dart';
class _Index<T> extends Parser<T> {
final ListParser<T> parser;
final int index;
_Index(this.parser, this.index);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
Object? value;
if (result.successful) {
var vList = result.value;
if (vList == null) {
throw ArgumentError('ParseResult is null');
}
if (index == -1) {
value = vList.last;
} else {
if (index < vList.length) {
// print(">>>>Index: $index, Size: ${vList.length}");
// value =
// index == -1 ? result.value!.last : result.value!.elementAt(index);
value = result.value!.elementAt(index);
}
}
}
return ParseResult<T>(
args.trampoline,
args.scanner,
this,
result.successful,
result.errors,
span: result.span,
value: value as T?,
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('index($index) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,119 @@
part of 'combinator.dart';
/// Matches any one of the given [parsers].
///
/// You can provide a custom [errorMessage].
Parser<T> longest<T>(Iterable<Parser<T>> parsers,
{Object? errorMessage, SyntaxErrorSeverity? severity}) {
return _Longest(parsers, errorMessage, severity ?? SyntaxErrorSeverity.error);
}
class _Longest<T> extends Parser<T> {
final Iterable<Parser<T>> parsers;
final Object? errorMessage;
final SyntaxErrorSeverity severity;
_Longest(this.parsers, this.errorMessage, this.severity);
@override
ParseResult<T> _parse(ParseArgs args) {
var inactive = parsers
.toList()
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
if (inactive.isEmpty) {
return ParseResult(args.trampoline, args.scanner, this, false, []);
}
var replay = args.scanner.position;
var errors = <SyntaxError>[];
var results = <ParseResult<T>>[];
for (var parser in inactive) {
var result = parser._parse(args.increaseDepth());
if (result.successful && result.span != null) {
results.add(result);
} else if (parser is _Alt) {
errors.addAll(result.errors);
}
args.scanner.position = replay;
}
if (results.isNotEmpty) {
results.sort((a, b) => b.span!.length.compareTo(a.span!.length));
args.scanner.scan(results.first.span!.text);
return results.first;
}
if (errorMessage != false) {
errors.add(
SyntaxError(
severity,
errorMessage?.toString() ??
'No match found for ${parsers.length} alternative(s)',
args.scanner.emptySpan,
),
);
}
return ParseResult(args.trampoline, args.scanner, this, false, errors);
}
@override
ParseResult<T> __parse(ParseArgs args) {
var replay = args.scanner.position;
var errors = <SyntaxError>[];
var results = <ParseResult<T>>[];
for (var parser in parsers) {
var result = parser._parse(args.increaseDepth());
if (result.successful) {
results.add(result);
} else if (parser is _Alt) {
errors.addAll(result.errors);
}
args.scanner.position = replay;
}
if (results.isNotEmpty) {
results.sort((a, b) => b.span!.length.compareTo(a.span!.length));
args.scanner.scan(results.first.span!.text);
return results.first;
}
errors.add(
SyntaxError(
severity,
errorMessage?.toString() ??
'No match found for ${parsers.length} alternative(s)',
args.scanner.emptySpan,
),
);
return ParseResult(args.trampoline, args.scanner, this, false, errors);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('longest(${parsers.length}) (')
..indent();
var i = 1;
for (var parser in parsers) {
buffer
..writeln('#${i++}:')
..indent();
parser.stringify(buffer);
buffer.outdent();
}
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,56 @@
part of 'combinator.dart';
class _Map<T, U> extends Parser<U> {
final Parser<T> parser;
final U Function(ParseResult<T>) f;
_Map(this.parser, this.f);
@override
ParseResult<U> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
return ParseResult<U>(
args.trampoline,
args.scanner,
this,
result.successful,
result.errors,
span: result.span,
value: result.successful ? f(result) : null,
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('map<$U> (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}
class _Change<T, U> extends Parser<U> {
final Parser<T> parser;
final ParseResult<U> Function(ParseResult<T>) f;
_Change(this.parser, this.f);
@override
ParseResult<U> __parse(ParseArgs args) {
return f(parser._parse(args.increaseDepth())).change(parser: this);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('change($f) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,41 @@
part of 'combinator.dart';
/// Expects to match a given [pattern]. If it is not matched, you can provide a custom [errorMessage].
Parser<T> match<T>(Pattern pattern,
{String? errorMessage, SyntaxErrorSeverity? severity}) =>
_Match<T>(pattern, errorMessage, severity ?? SyntaxErrorSeverity.error);
class _Match<T> extends Parser<T> {
final Pattern pattern;
final String? errorMessage;
final SyntaxErrorSeverity severity;
_Match(this.pattern, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
var scanner = args.scanner;
if (!scanner.scan(pattern)) {
return ParseResult(args.trampoline, scanner, this, false, [
SyntaxError(
severity,
errorMessage ?? 'Expected "$pattern".',
scanner.emptySpan,
),
]);
}
return ParseResult<T>(
args.trampoline,
scanner,
this,
true,
[],
span: scanner.lastSpan,
);
}
@override
void stringify(CodeBuffer buffer) {
buffer.writeln('match($pattern)');
}
}

View file

@ -0,0 +1,28 @@
part of 'combinator.dart';
class _MaxDepth<T> extends Parser<T> {
final Parser<T> parser;
final int cap;
_MaxDepth(this.parser, this.cap);
@override
ParseResult<T> __parse(ParseArgs args) {
if (args.depth > cap) {
return ParseResult<T>(args.trampoline, args.scanner, this, false, []);
}
return parser._parse(args.increaseDepth());
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('max depth($cap) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,51 @@
part of 'combinator.dart';
class _Negate<T> extends Parser<T> {
final Parser<T> parser;
final String? errorMessage;
final SyntaxErrorSeverity severity;
_Negate(this.parser, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
if (!result.successful) {
return ParseResult<T>(
args.trampoline,
args.scanner,
this,
true,
[],
span: result.span ?? args.scanner.lastSpan ?? args.scanner.emptySpan,
value: result.value,
);
}
result = result.change(successful: false);
if (errorMessage != null) {
result = result.addErrors([
SyntaxError(
severity,
errorMessage,
result.span,
),
]);
}
return result;
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('negate (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,57 @@
part of 'combinator.dart';
class _Opt<T> extends Parser<T> {
final Parser<T> parser;
final bool backtrack;
_Opt(this.parser, this.backtrack);
@override
ParseResult<T> __parse(ParseArgs args) {
var replay = args.scanner.position;
var result = parser._parse(args.increaseDepth());
if (!result.successful) args.scanner.position = replay;
return result.change(parser: this, successful: true);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('optional (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}
class _ListOpt<T> extends ListParser<T> {
final ListParser<T> parser;
final bool backtrack;
_ListOpt(this.parser, this.backtrack);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
var replay = args.scanner.position;
var result = parser._parse(args.increaseDepth());
if (!result.successful) args.scanner.position = replay;
return result.change(parser: this, successful: true);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('optional (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,142 @@
part of 'combinator.dart';
/*
/// Handles left recursion in a grammar using the Pratt algorithm.
class Recursion<T> {
Iterable<Parser<T>> prefix;
Map<Parser, T Function(T, T, ParseResult<T>)> infix;
Map<Parser, T Function(T, T, ParseResult<T>)> postfix;
Recursion({this.prefix, this.infix, this.postfix}) {
prefix ??= [];
infix ??= {};
postfix ??= {};
}
Parser<T> precedence(int p) => _Precedence(this, p);
void stringify(CodeBuffer buffer) {
buffer
..writeln('recursion (')
..indent()
..writeln('prefix(${prefix.length}')
..writeln('infix(${infix.length}')
..writeln('postfix(${postfix.length}')
..outdent()
..writeln(')');
}
}
class _Precedence<T> extends Parser<T> {
final Recursion r;
final int precedence;
_Precedence(this.r, this.precedence);
@override
ParseResult<T> __parse(ParseArgs args) {
int replay = args.scanner.position;
var errors = <SyntaxError>[];
var start = args.scanner.state;
var reversedKeys = r.infix.keys.toList().reversed;
for (var pre in r.prefix) {
var result = pre._parse(args.increaseDepth()), originalResult = result;
if (!result.successful) {
if (pre is _Alt) errors.addAll(result.errors);
args.scanner.position = replay;
} else {
var left = result.value;
replay = args.scanner.position;
//print('${result.span.text}:\n' + scanner.emptySpan.highlight());
while (true) {
bool matched = false;
//for (int i = 0; i < r.infix.length; i++) {
for (int i = r.infix.length - 1; i >= 0; i--) {
//var fix = r.infix.keys.elementAt(r.infix.length - i - 1);
var fix = reversedKeys.elementAt(i);
if (i < precedence) continue;
var result = fix._parse(args.increaseDepth());
if (!result.successful) {
if (fix is _Alt) errors.addAll(result.errors);
// If this is the last alternative and it failed, don't continue looping.
//if (true || i + 1 < r.infix.length)
args.scanner.position = replay;
} else {
//print('FOUND $fix when left was $left');
//print('$i vs $precedence\n${originalResult.span.highlight()}');
result = r.precedence(i)._parse(args.increaseDepth());
if (!result.successful) {
} else {
matched = false;
var old = left;
left = r.infix[fix](left, result.value, result);
print(
'$old $fix ${result.value} = $left\n${result.span.highlight()}');
break;
}
}
}
if (!matched) break;
}
replay = args.scanner.position;
//print('f ${result.span.text}');
for (var post in r.postfix.keys) {
var result = pre._parse(args.increaseDepth());
if (!result.successful) {
if (post is _Alt) errors.addAll(result.errors);
args.scanner.position = replay;
} else {
left = r.infix[post](left, originalResult.value, result);
}
}
if (!args.scanner.isDone) {
// If we're not done scanning, then we need some sort of guard to ensure the
// that this exact parser does not run again in the exact position.
}
return ParseResult(
args.trampoline,
args.scanner,
this,
true,
errors,
value: left,
span: args.scanner.spanFrom(start),
);
}
}
return ParseResult(
args.trampoline,
args.scanner,
this,
false,
errors,
span: args.scanner.spanFrom(start),
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('precedence($precedence) (')
..indent();
r.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}
*/

View file

@ -0,0 +1,46 @@
part of 'combinator.dart';
class _Reduce<T> extends Parser<T> {
final ListParser<T> parser;
final T Function(T, T) combine;
_Reduce(this.parser, this.combine);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
if (!result.successful) {
return ParseResult<T>(
args.trampoline,
args.scanner,
this,
false,
result.errors,
);
}
result = result.change(
value: result.value?.isNotEmpty == true ? result.value : []);
return ParseResult<T>(
args.trampoline,
args.scanner,
this,
result.successful,
[],
span: result.span,
value: result.value!.isEmpty ? null : result.value!.reduce(combine),
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('reduce($combine) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,44 @@
part of 'combinator.dart';
Reference<T> reference<T>() => Reference<T>._();
class Reference<T> extends Parser<T> {
Parser<T>? _parser;
bool printed = false;
Reference._();
set parser(Parser<T> value) {
if (_parser != null) {
throw StateError('There is already a parser assigned to this reference.');
}
_parser = value;
}
@override
ParseResult<T> __parse(ParseArgs args) {
if (_parser == null) {
throw StateError('There is no parser assigned to this reference.');
}
return _parser!._parse(args);
}
@override
ParseResult<T> _parse(ParseArgs args) {
if (_parser == null) {
throw StateError('There is no parser assigned to this reference.');
}
return _parser!._parse(args);
}
@override
void stringify(CodeBuffer buffer) {
if (_parser == null) {
buffer.writeln('(undefined reference <$T>)');
} else if (!printed) {
_parser!.stringify(buffer);
}
printed = true;
buffer.writeln('(previously printed reference)');
}
}

View file

@ -0,0 +1,91 @@
part of 'combinator.dart';
class _Repeat<T> extends ListParser<T> {
final Parser<T> parser;
final int count;
final bool exact, backtrack;
final String tooFew;
final String tooMany;
final SyntaxErrorSeverity severity;
_Repeat(this.parser, this.count, this.exact, this.tooFew, this.tooMany,
this.backtrack, this.severity);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
var errors = <SyntaxError>[];
var results = <T>[];
var spans = <FileSpan>[];
var success = 0;
var replay = args.scanner.position;
ParseResult<T> result;
do {
result = parser._parse(args.increaseDepth());
if (result.successful) {
success++;
if (result.value != null) {
results.add(result.value as T);
}
replay = args.scanner.position;
} else if (backtrack) {
args.scanner.position = replay;
}
if (result.span != null) {
spans.add(result.span!);
}
} while (result.successful);
if (success < count) {
errors.addAll(result.errors);
errors.add(
SyntaxError(
severity,
tooFew,
result.span ?? args.scanner.emptySpan,
),
);
if (backtrack) args.scanner.position = replay;
return ParseResult<List<T>>(
args.trampoline, args.scanner, this, false, errors);
} else if (success > count && exact) {
if (backtrack) args.scanner.position = replay;
return ParseResult<List<T>>(args.trampoline, args.scanner, this, false, [
SyntaxError(
severity,
tooMany,
result.span ?? args.scanner.emptySpan,
),
]);
}
var span = spans.reduce((a, b) => a.expand(b));
return ParseResult<List<T>>(
args.trampoline,
args.scanner,
this,
true,
[],
span: span,
value: results,
);
}
@override
void stringify(CodeBuffer buffer) {
var r = StringBuffer('{$count');
if (!exact) r.write(',');
r.write('}');
buffer
..writeln('repeat($r) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,47 @@
part of 'combinator.dart';
class _Safe<T> extends Parser<T> {
final Parser<T> parser;
final bool backtrack;
final String errorMessage;
final SyntaxErrorSeverity severity;
bool _triggered = false;
_Safe(this.parser, this.backtrack, this.errorMessage, this.severity);
@override
ParseResult<T> __parse(ParseArgs args) {
var replay = args.scanner.position;
try {
if (_triggered) throw Exception();
return parser._parse(args.increaseDepth());
} catch (_) {
_triggered = true;
if (backtrack) args.scanner.position = replay;
var errors = <SyntaxError>[];
errors.add(
SyntaxError(
severity,
errorMessage,
args.scanner.lastSpan ?? args.scanner.emptySpan,
),
);
return ParseResult<T>(args.trampoline, args.scanner, this, false, errors);
}
}
@override
void stringify(CodeBuffer buffer) {
var t = _triggered ? 'triggered' : 'not triggered';
buffer
..writeln('safe($t) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,41 @@
part of 'combinator.dart';
class _ToList<T> extends ListParser<T> {
final Parser<T> parser;
_ToList(this.parser);
@override
ParseResult<List<T>> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth());
if (result.value is List) {
return (result as ParseResult<List<T>>).change(parser: this);
}
var values = <T>[];
if (result.value != null) {
values.add(result.value as T);
}
return ParseResult(
args.trampoline,
args.scanner,
this,
result.successful,
result.errors,
span: result.span,
value: values,
);
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('to list (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,57 @@
part of 'combinator.dart';
/// A typed parser that parses a sequence of 2 values of different types.
Parser<Tuple2<A, B>> tuple2<A, B>(Parser<A> a, Parser<B> b) {
return chain([a, b]).map((r) {
return Tuple2(r.value?[0] as A, r.value?[1] as B);
});
}
/// A typed parser that parses a sequence of 3 values of different types.
Parser<Tuple3<A, B, C>> tuple3<A, B, C>(Parser<A> a, Parser<B> b, Parser<C> c) {
return chain([a, b, c]).map((r) {
return Tuple3(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C);
});
}
/// A typed parser that parses a sequence of 4 values of different types.
Parser<Tuple4<A, B, C, D>> tuple4<A, B, C, D>(
Parser<A> a, Parser<B> b, Parser<C> c, Parser<D> d) {
return chain([a, b, c, d]).map((r) {
return Tuple4(
r.value?[0] as A, r.value?[1] as B, r.value?[2] as C, r.value?[3] as D);
});
}
/// A typed parser that parses a sequence of 5 values of different types.
Parser<Tuple5<A, B, C, D, E>> tuple5<A, B, C, D, E>(
Parser<A> a, Parser<B> b, Parser<C> c, Parser<D> d, Parser<E> e) {
return chain([a, b, c, d, e]).map((r) {
return Tuple5(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C,
r.value?[3] as D, r.value?[4] as E);
});
}
/// A typed parser that parses a sequence of 6 values of different types.
Parser<Tuple6<A, B, C, D, E, F>> tuple6<A, B, C, D, E, F>(Parser<A> a,
Parser<B> b, Parser<C> c, Parser<D> d, Parser<E> e, Parser<F> f) {
return chain([a, b, c, d, e, f]).map((r) {
return Tuple6(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C,
r.value?[3] as D, r.value?[4] as E, r.value?[5] as F);
});
}
/// A typed parser that parses a sequence of 7 values of different types.
Parser<Tuple7<A, B, C, D, E, F, G>> tuple7<A, B, C, D, E, F, G>(
Parser<A> a,
Parser<B> b,
Parser<C> c,
Parser<D> d,
Parser<E> e,
Parser<F> f,
Parser<G> g) {
return chain([a, b, c, d, e, f, g]).map((r) {
return Tuple7(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C,
r.value?[3] as D, r.value?[4] as E, r.value?[5] as F, r.value?[6] as G);
});
}

View file

@ -0,0 +1,25 @@
part of 'combinator.dart';
class _Value<T> extends Parser<T> {
final Parser<T> parser;
final T Function(ParseResult<T>) f;
_Value(this.parser, this.f);
@override
ParseResult<T> __parse(ParseArgs args) {
var result = parser._parse(args.increaseDepth()).change(parser: this);
return result.successful ? result.change(value: f(result)) : result;
}
@override
void stringify(CodeBuffer buffer) {
buffer
..writeln('set value($f) (')
..indent();
parser.stringify(buffer);
buffer
..outdent()
..writeln(')');
}
}

View file

@ -0,0 +1,23 @@
import 'package:source_span/source_span.dart';
class SyntaxError implements Exception {
final SyntaxErrorSeverity severity;
final String? message;
final FileSpan? span;
String? _toolString;
SyntaxError(this.severity, this.message, this.span);
String? get toolString {
if (_toolString != null) return _toolString;
var type = severity == SyntaxErrorSeverity.warning ? 'warning' : 'error';
return _toolString = '$type: ${span!.start.toolString}: $message';
}
}
enum SyntaxErrorSeverity {
warning,
error,
info,
hint,
}

View file

@ -0,0 +1,425 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: "direct main"
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
platform_code_buffer:
dependency: "direct main"
description:
path: "../code_buffer"
relative: true
source: path
version: "5.2.0"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: "direct main"
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: "direct main"
description:
name: string_scanner
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test:
dependency: "direct dev"
description:
name: test
sha256: "22eb7769bee38c7e032d532e8daa2e1cc901b799f603550a4db8f3a5f5173ea2"
url: "https://pub.dev"
source: hosted
version: "1.25.12"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
tuple:
dependency: "direct main"
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"

View file

@ -0,0 +1,15 @@
name: platform_combinator
version: 5.2.0
description: Packrat parser combinators that support static typing, generics, file spans, memoization, and more.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/combinator
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
platform_code_buffer: ^5.0.0
matcher: ^0.12.10
source_span: ^1.8.1
string_scanner: ^1.1.0
tuple: ^2.0.0
dev_dependencies:
test: ^1.24.0
lints: ^4.0.0

View file

@ -0,0 +1,12 @@
import 'package:test/test.dart';
import 'list_test.dart' as list;
import 'match_test.dart' as match;
import 'misc_test.dart' as misc;
import 'value_test.dart' as value;
void main() {
group('list', list.main);
group('match', match.main);
group('value', value.main);
misc.main();
}

View file

@ -0,0 +1,3 @@
import 'package:string_scanner/string_scanner.dart';
SpanScanner scan(String text) => SpanScanner(text);

View file

@ -0,0 +1,22 @@
import 'package:platform_combinator/combinator.dart';
import 'package:test/test.dart';
import 'common.dart';
void main() {
var number = chain([
match(RegExp(r'[0-9]+')).value((r) => int.parse(r.span!.text)),
match(',').opt(),
]).first().cast<int>();
var numbers = number.plus();
test('sort', () {
var parser = numbers.sort((a, b) => a.compareTo(b));
expect(parser.parse(scan('21,2,3,34,20')).value, [2, 3, 20, 21, 34]);
});
test('reduce', () {
var parser = numbers.reduce((a, b) => a + b);
expect(parser.parse(scan('21,2,3,34,20')).value, 80);
expect(parser.parse(scan('not numbers')).value, isNull);
});
}

View file

@ -0,0 +1,16 @@
import 'package:platform_combinator/combinator.dart';
import 'package:test/test.dart';
import 'common.dart';
void main() {
test('match string', () {
expect(match('hello').parse(scan('hello world')).successful, isTrue);
});
test('match start only', () {
expect(match('hello').parse(scan('goodbye hello')).successful, isFalse);
});
test('fail if no match', () {
expect(match('hello').parse(scan('world')).successful, isFalse);
});
}

View file

@ -0,0 +1,65 @@
import 'package:platform_combinator/combinator.dart';
import 'package:test/test.dart';
import 'common.dart';
void main() {
test('advance', () {
var scanner = scan('hello world');
// Casted -> dynamic just for the sake of coverage.
var parser = match('he').forward(2).castDynamic();
parser.parse(scanner);
expect(scanner.position, 4);
});
test('change', () {
var parser = match('hello').change((r) => r.change(value: 23));
expect(parser.parse(scan('helloworld')).value, 23);
});
test('check', () {
var parser = match<int>(RegExp(r'[A-Za-z]+'))
.value((r) => r.span!.length)
.check(greaterThan(3));
expect(parser.parse(scan('helloworld')).successful, isTrue);
expect(parser.parse(scan('yo')).successful, isFalse);
});
test('map', () {
var parser = match(RegExp(r'[A-Za-z]+')).map<int>((r) => r.span!.length);
expect(parser.parse(scan('hello')).value, 5);
});
test('negate', () {
var parser = match('hello').negate(errorMessage: 'world');
expect(parser.parse(scan('goodbye world')).successful, isTrue);
expect(parser.parse(scan('hello world')).successful, isFalse);
expect(parser.parse(scan('hello world')).errors.first.message, 'world');
});
group('opt', () {
var single = match('hello').opt(backtrack: true);
var list = match('hel').then(match('lo')).opt();
test('succeeds if present', () {
expect(single.parse(scan('hello')).successful, isTrue);
expect(list.parse(scan('hello')).successful, isTrue);
});
test('succeeds if not present', () {
expect(single.parse(scan('goodbye')).successful, isTrue);
expect(list.parse(scan('goodbye')).successful, isTrue);
});
test('backtracks if not present', () {
for (var parser in [single, list]) {
var scanner = scan('goodbye');
var pos = scanner.position;
parser.parse(scanner);
expect(scanner.position, pos);
}
});
});
test('safe', () {});
}

View file

@ -0,0 +1,53 @@
void main() {}
/*
void main() {
var number = match( RegExp(r'-?[0-9]+(\.[0-9]+)?'))
.map<num>((r) => num.parse(r.span.text));
var term = reference<num>();
var r = Recursion<num>();
r.prefix = [number];
r.infix.addAll({
match('*'): (l, r, _) => l * r,
match('/'): (l, r, _) => l / r,
match('+'): (l, r, _) => l + r,
match('-'): (l, r, _) => l - r,
match('-'): (l, r, _) => l - r,
match('+'): (l, r, _) => l + r,
match('/'): (l, r, _) => l / r,
match('*'): (l, r, _) => l * r,
});
term.parser = r.precedence(0);
num parse(String text) {
var scanner = SpanScanner(text);
var result = term.parse(scanner);
print(result.span.highlight());
return result.value;
}
test('prefix', () {
expect(parse('24'), 24);
});
test('infix', () {
expect(parse('12/6'), 2);
expect(parse('24+23'), 47);
expect(parse('24-23'), 1);
expect(parse('4*3'), 12);
});
test('precedence', () {
expect(parse('2+3*5*2'), 15);
//expect(parse('2+3+5-2*2'), 15);
});
}
*/

View file

@ -0,0 +1,15 @@
import 'package:platform_combinator/combinator.dart';
import 'package:test/test.dart';
import 'common.dart';
void main() {
var parser = match('hello').value((r) => 'world');
test('sets value', () {
expect(parser.parse(scan('hello world')).value, 'world');
});
test('no value if no match', () {
expect(parser.parse(scan('goodbye world')).value, isNull);
});
}

View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,66 @@
# Change Log
## 5.2.0
* Require Dart >= 3.3
* Updated `lints` to 4.0.0
## 5.1.0
* Updated `lints` to 3.0.0
## 5.0.0
* Require Dart >= 3.0
## 5.0.0-beta.1
* Require Dart >= 3.0
## 4.0.0
* Require Dart >= 2.17
* Added hashCode
## 3.0.2
* Fixed license link
## 3.0.1
* Update broken link in pubspec
## 3.0.0
* Upgraded from `pendantic` to `lints` linter
* Removed deprecated parameters
* Published as `platform_html_builder` package
## 2.0.3
* Added an example
* Updated README
## 2.0.2
* Run `dartfmt -w .`
## 2.0.1
* Added pedantic dart rules
## 2.0.0
* Migrated to work with Dart SDK 2.12.x NNBD
## 1.0.4
* Added `rebuild`, `rebuildRecursive`, and `NodeBuilder`.
## 1.0.3
* Dart 2 ready!
## 1.0.2
* Changed `h` and the `Node` constructor to take `Iterable`s of children, instead of just `List`s.

View file

@ -0,0 +1,113 @@
# Betaluk Html Builder
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/platform_html_builder?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![License](https://img.shields.io/github/license/dart-backend/belatuk-common-utilities)](https://github.com/dart-backend/belatuk-common-utilities/blob/main/packages/html_builder/LICENSE)
**Replacement of `package:html_builder` with breaking changes to support NNBD.**
This package builds HTML AST's and renders them to HTML. It can be used as an internal DSL, i.e. for a templating engine.
## Installation
In your `pubspec.yaml`:
```yaml
dependencies:
platform_html_builder: ^5.1.0
```
## Usage
```dart
import 'package:platform_html_builder/platform_html_builder.dart';
void main() {
// Akin to React.createElement(...);
var $el = h('my-element', p: {}, c: []);
// Attributes can be plain Strings.
h('foo', p: {
'bar': 'baz'
});
// Null attributes do not appear.
h('foo', p: {
'does-not-appear': null
});
// If an attribute is a bool, then it will only appear if its value is true.
h('foo', p: {
'appears': true,
'does-not-appear': false
});
// Or, a String or Map.
h('foo', p: {
'style': 'background-color: white; color: red;'
});
h('foo', p: {
'style': {
'background-color': 'white',
'color': 'red'
}
});
// Or, a String or Iterable.
h('foo', p: {
'class': 'a b'
});
h('foo', p: {
'class': ['a', 'b']
});
}
```
Standard HTML5 elements:
```dart
import 'package:platform_html_builder/elements.dart';
void main() {
var $dom = html(lang: 'en', c: [
head(c: [
title(c: [text('Hello, world!')])
]),
body(c: [
h1(c: [text('Hello, world!')]),
p(c: [text('Ok')])
])
]);
}
```
Rendering to HTML:
```dart
String html = StringRenderer().render($dom);
```
Example implementation with the [Angel3](https://pub.dev/packages/angel3_framework) backend framework,
which uses [dedicated html_builder package](https://github.com/dukefirehawk/angel/tree/html):
```dart
import 'dart:io';
import 'package:belatuk_framework/belatuk_framework.dart';
import 'package:platform_html_builder/elements.dart';
configureViews(Angel app) async {
app.get('/foo/:id', (req, res) async {
var foo = await app.service('foo').read(req.params['id']);
return html(c: [
head(c: [
title(c: [text(foo.name)])
]),
body(c: [
h1(c: [text(foo.name)])
])
]);
});
}
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,15 @@
import 'package:platform_html_builder/elements.dart';
void main() {
var dom = html(lang: 'en', c: [
head(c: [
title(c: [text('Hello, world!')])
]),
body(c: [
h1(c: [text('Hello, world!')]),
p(c: [text('Ok')])
])
]);
print(dom);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
export 'src/mutations.dart';
export 'src/node.dart';
export 'src/node_builder.dart';
export 'src/renderer.dart';

View file

@ -0,0 +1,20 @@
import 'node.dart';
import 'node_builder.dart';
/// Returns a function that rebuilds an arbitrary [Node] by applying the [transform] to it.
Node Function(Node) rebuild(NodeBuilder Function(NodeBuilder) transform,
{bool selfClosing = false}) {
return (node) =>
transform(NodeBuilder.from(node)).build(selfClosing: selfClosing);
}
/// Applies [f] to all children of this node, recursively.
///
/// Use this alongside [rebuild].
Node Function(Node) rebuildRecursive(Node Function(Node) f) {
Node build(Node node) {
return NodeBuilder.from(f(node)).mapChildren(build).build();
}
return build;
}

View file

@ -0,0 +1,73 @@
import 'package:collection/collection.dart';
/// Shorthand function to generate a new [Node].
Node h(String tagName,
[Map<String, dynamic> attributes = const {},
Iterable<Node> children = const []]) =>
Node(tagName, attributes, children);
/// Represents an HTML node.
class Node {
final String tagName;
final Map<String, dynamic> attributes = {};
final List<Node> children = [];
Node(this.tagName,
[Map<String, dynamic> attributes = const {},
Iterable<Node> children = const []]) {
this
..attributes.addAll(attributes)
..children.addAll(children);
}
Node._selfClosing(this.tagName,
[Map<String, dynamic> attributes = const {}]) {
this.attributes.addAll(attributes);
}
@override
bool operator ==(other) {
return other is Node &&
other.tagName == tagName &&
const ListEquality<Node>().equals(other.children, children) &&
const MapEquality<String, dynamic>()
.equals(other.attributes, attributes);
}
@override
int get hashCode {
int hash = Object.hash(tagName, Object.hashAll(children));
return Object.hash(hash, Object.hashAll(attributes.values));
}
}
/// Represents a self-closing tag, i.e. `<br>`.
class SelfClosingNode extends Node {
/*
@override
final String tagName;
@override
final Map<String, dynamic> attributes = {};
*/
@override
List<Node> get children => List<Node>.unmodifiable([]);
// ignore: use_super_parameters
SelfClosingNode(String tagName, [Map<String, dynamic> attributes = const {}])
: super._selfClosing(tagName, attributes);
}
/// Represents a text node.
class TextNode extends Node {
final String text;
TextNode(this.text) : super(':text');
@override
bool operator ==(other) => other is TextNode && other.text == text;
@override
int get hashCode => text.hashCode;
}

View file

@ -0,0 +1,108 @@
import 'node.dart';
/// Helper class to build nodes.
class NodeBuilder {
final String tagName;
final Map<String, dynamic> attributes;
final Iterable<Node> children;
Node? _existing;
NodeBuilder(this.tagName,
{this.attributes = const {}, this.children = const []});
/// Creates a [NodeBuilder] that just spits out an already-existing [Node].
factory NodeBuilder.existing(Node existingNode) =>
NodeBuilder(existingNode.tagName).._existing = existingNode;
factory NodeBuilder.from(Node node) => NodeBuilder(node.tagName,
attributes: Map<String, dynamic>.from(node.attributes),
children: List<Node>.from(node.children));
/// Builds the node.
Node build({bool selfClosing = false}) =>
_existing ??
(selfClosing
? SelfClosingNode(tagName, attributes)
: Node(tagName, attributes, children));
/// Produce a modified copy of this builder.
NodeBuilder change(
{String? tagName,
Map<String, dynamic>? attributes,
Iterable<Node>? children}) {
return NodeBuilder(tagName ?? this.tagName,
attributes: attributes ?? this.attributes,
children: children ?? this.children);
}
NodeBuilder changeTagName(String tagName) => change(tagName: tagName);
NodeBuilder changeAttributes(Map<String, dynamic> attributes) =>
change(attributes: attributes);
NodeBuilder changeChildren(Iterable<Node> children) =>
change(children: children);
NodeBuilder changeAttributesMapped(
Map<String, dynamic> Function(Map<String, dynamic>) f) {
var map = Map<String, dynamic>.from(attributes);
return changeAttributes(f(map));
}
NodeBuilder changeChildrenMapped(Iterable<Node> Function(List<Node>) f) {
var list = List<Node>.from(children);
return changeChildren(f(list));
}
NodeBuilder mapChildren(Node Function(Node) f) =>
changeChildrenMapped((list) => list.map(f));
NodeBuilder mapAttributes(
MapEntry<String, dynamic> Function(String, dynamic) f) =>
changeAttributesMapped((map) => map.map(f));
NodeBuilder setAttribute(String name, dynamic value) =>
changeAttributesMapped((map) => map..[name] = value);
NodeBuilder addChild(Node child) =>
changeChildrenMapped((list) => list..add(child));
NodeBuilder removeChild(Node child) =>
changeChildrenMapped((list) => list..remove(child));
NodeBuilder removeAttribute(String name) =>
changeAttributesMapped((map) => map..remove(name));
NodeBuilder setId(String id) => setAttribute('id', id);
NodeBuilder setClassName(String className) =>
setAttribute('class', className);
NodeBuilder setClasses(Iterable<String> classes) =>
setClassName(classes.join(' '));
NodeBuilder setClassesMapped(Iterable<String> Function(List<String>) f) {
var clazz = attributes['class'];
var classes = <String>[];
if (clazz is String) {
classes.addAll(clazz.split(' '));
} else if (clazz is Iterable) {
classes.addAll(clazz.map((s) => s.toString()));
}
return setClasses(f(classes));
}
NodeBuilder addClass(String className) => setClassesMapped(
(classes) => classes.contains(className) ? classes : classes
..add(className));
NodeBuilder removeClass(String className) =>
setClassesMapped((classes) => classes..remove(className));
NodeBuilder toggleClass(String className) =>
setClassesMapped((classes) => classes.contains(className)
? (classes..remove(className))
: (classes..add(className)));
}

View file

@ -0,0 +1,140 @@
import 'node.dart';
/// An object that can render a DOM tree into another representation, i.e. a `String`.
abstract class Renderer<T> {
/// Renders a DOM tree into another representation.
T render(Node rootNode);
}
/// Renders a DOM tree into a HTML string.
abstract class StringRenderer implements Renderer<String> {
/// Initializes a new [StringRenderer].
///
/// If [html5] is not `false` (default: `true`), then self-closing elements will be rendered with a slash before the last angle bracket, ex. `<br />`.
/// If [pretty] is `true` (default), then [whitespace] (default: `' '`) will be inserted between nodes.
/// You can also provide a [doctype] (default: `html`).
factory StringRenderer(
{bool html5 = true,
bool pretty = true,
String doctype = 'html',
String whitespace = ' '}) =>
pretty == true
? _PrettyStringRendererImpl(
html5: html5 != false, doctype: doctype, whitespace: whitespace)
: _StringRendererImpl(html5: html5 != false, doctype: doctype);
}
class _StringRendererImpl implements StringRenderer {
final String? doctype;
final bool? html5;
_StringRendererImpl({this.html5, this.doctype});
void _renderInto(Node node, StringBuffer buf) {
if (node is TextNode) {
buf.write(node.text);
} else {
buf.write('<${node.tagName}');
node.attributes.forEach((k, v) {
if (v == true) {
buf.write(' $k');
} else if (v == false || v == null) {
// Ignore
} else if (v is Iterable) {
var val = v.join(' ').replaceAll('"', '\\"');
buf.write(' $k="$val"');
} else if (v is Map) {
var val = v.keys
.fold<String>('', (out, k) => out += '$k: ${v[k]};')
.replaceAll('"', '\\"');
buf.write(' $k="$val"');
} else {
var val = v.toString().replaceAll('"', '\\"');
buf.write(' $k="$val"');
}
});
if (node is SelfClosingNode) {
buf.write((html5 != false) ? '>' : '/>');
} else {
buf.write('>');
for (var child in node.children) {
_renderInto(child, buf);
}
buf.write('</${node.tagName}>');
}
}
}
@override
String render(Node rootNode) {
var buf = StringBuffer();
if (doctype?.isNotEmpty == true) buf.write('<!DOCTYPE $doctype>');
_renderInto(rootNode, buf);
return buf.toString();
}
}
class _PrettyStringRendererImpl implements StringRenderer {
final bool? html5;
final String? doctype, whitespace;
_PrettyStringRendererImpl({this.html5, this.whitespace, this.doctype});
void _applyTabs(int tabs, StringBuffer buf) {
for (var i = 0; i < tabs; i++) {
buf.write(whitespace ?? ' ');
}
}
void _renderInto(int tabs, Node node, StringBuffer buf) {
if (tabs > 0) buf.writeln();
_applyTabs(tabs, buf);
if (node is TextNode) {
buf.write(node.text);
} else {
buf.write('<${node.tagName}');
node.attributes.forEach((k, v) {
if (v == true) {
buf.write(' $k');
} else if (v == false || v == null) {
// Ignore
} else if (v is Iterable) {
var val = v.join(' ').replaceAll('"', '\\"');
buf.write(' $k="$val"');
} else if (v is Map) {
var val = v.keys
.fold<String>('', (out, k) => out += '$k: ${v[k]};')
.replaceAll('"', '\\"');
buf.write(' $k="$val"');
} else {
var val = v.toString().replaceAll('"', '\\"');
buf.write(' $k="$val"');
}
});
if (node is SelfClosingNode) {
buf.write((html5 != false) ? '>' : '/>');
} else {
buf.write('>');
for (var child in node.children) {
_renderInto(tabs + 1, child, buf);
}
buf.writeln();
_applyTabs(tabs, buf);
buf.write('</${node.tagName}>');
}
}
}
@override
String render(Node rootNode) {
var buf = StringBuffer();
if (doctype?.isNotEmpty == true) buf.writeln('<!DOCTYPE $doctype>');
_renderInto(0, rootNode, buf);
return buf.toString();
}
}

View file

@ -0,0 +1,418 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
collection:
dependency: "direct main"
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
url: "https://pub.dev"
source: hosted
version: "1.11.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
html:
dependency: "direct dev"
description:
name: html
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
url: "https://pub.dev"
source: hosted
version: "0.15.5"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.7.1"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67
url: "https://pub.dev"
source: hosted
version: "2.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test:
dependency: "direct dev"
description:
name: test
sha256: "22eb7769bee38c7e032d532e8daa2e1cc901b799f603550a4db8f3a5f5173ea2"
url: "https://pub.dev"
source: hosted
version: "1.25.12"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
version: "0.6.8"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.5.0 <4.0.0"

View file

@ -0,0 +1,12 @@
name: platform_html_builder
version: 5.2.0
description: Build HTML AST's and render them to HTML. This can be used as an internal DSL, i.e. for a templating engine.
homepage: https://github.com/dart-backend/belatuk-common-utilities/tree/main/packages/html_builder
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
collection: ^1.17.0
dev_dependencies:
html: ^0.15.0
test: ^1.24.0
lints: ^4.0.0

View file

@ -0,0 +1,33 @@
import 'package:html/parser.dart' as html5;
import 'package:platform_html_builder/elements.dart';
import 'package:test/test.dart';
void main() {
test('pretty', () {
var $dom = html(
lang: 'en',
c: [
head(c: [
title(c: [text('Hello, world!')])
]),
body(
p: {'unresolved': true},
c: [
h1(c: [text('Hello, world!')]),
br(),
hr(),
],
)
],
);
var rendered = StringRenderer().render($dom);
print(rendered);
var $parsed = html5.parse(rendered);
var $title = $parsed.querySelector('title')!;
expect($title.text.trim(), 'Hello, world!');
var $h1 = $parsed.querySelector('h1')!;
expect($h1.text.trim(), 'Hello, world!');
});
}

5
common/http_server/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.dart_tool
.packages
.metals
.vscode
pubspec.lock

View file

@ -0,0 +1,3 @@
{
"test_package": true
}

View file

@ -0,0 +1,9 @@
# Authors
## Current
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
## Previous
* Google Inc.

View file

@ -0,0 +1,112 @@
# Change Log
## 4.5.1
* Fixed linter warnings
## 4.5.0
* Require Dart >= 3.5
* Updated `lints` to 5.0.0
* Updated `mime` to 2.0.0
## 4.4.0
* Updated `lints` to 4.0.0
## 4.3.0
* Require Dart >= 3.3
## 4.2.0
* Upgraded `lints` to 3.0.0
## 4.1.2
* Fixed `_VirtualDirectoryFileStream` class due to change in Dart 3.0.3
## 4.1.1
* Updated README
## 4.1.0
* Upgraded `test_api` to 0.6.0
## 4.0.0
* Require Dart >= 3.0
## 4.0.0-beta.1
* Require Dart >= 3.0
* Fixed linter warnings
## 3.0.0
* Require Dart >= 2.17
* Fixed analyzer warnings
## 2.1.0
* Updated linter to `package:lints`
## 2.0.2
* Transfered repository to `dart-backend`
## 2.0.1
* Added example
## 2.0.0
* Migrated to `platform_http_server`
## 1.0.0
* Migrate to null safety.
* Allow multipart form data with specified encodings that don't require
decoding.
## 0.9.8+3
* Prepare for `HttpClientResponse` SDK change (implements `Stream<Uint8List>`
rather than `Stream<List<int>>`).
## 0.9.8+2
* Prepare for `File.openRead()` SDK change in signature.
## 0.9.8+1
* Fix a Dart 2 type issue.
## 0.9.8
* Updates to support Dart 2 constants.
## 0.9.7
* Updates to support Dart 2.0 core library changes (wave
2.2). See [issue 31847][sdk#31847] for details.
[sdk#31847]: https://github.com/dart-lang/sdk/issues/31847
## 0.9.6
* Updated the secure networking code to the SDKs version 1.15 SecurityContext api
## 0.9.5+1
* Updated the layout of package contents.
## 0.9.5
* Removed the decoding of HTML entity values (in the form &#xxxxx;) for
values when parsing multipart/form-post requests.
## 0.9.4
* Fixed bugs in the handling of the Range header

View file

@ -0,0 +1,3 @@
# Contributing
Any contributions from the community are welcome.

Some files were not shown because too many files have changed in this diff Show more