Skip to content

Commit

Permalink
Merge pull request #9 from liplum/master
Browse files Browse the repository at this point in the history
Refactor: Improve string quoting logic and fixed string-related bugs
  • Loading branch information
gmpassos authored Feb 15, 2025
2 parents cf1e95e + 9ae2af3 commit 6b87ab4
Show file tree
Hide file tree
Showing 14 changed files with 599 additions and 164 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ test/.test_cov.dart
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

.DS_Store
.DS_Store

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.1.0

- Refactored string quoting: enhanced the handling of unquoted strings.
- Resolved an issue where empty strings were not supported when using unquoted strings.
- Introduced `YamlWriter.config()` as a new constructor, along with the `YamlWriterConfig` class.

## 2.0.1

- Remove unnecessary trailing spaces from keys with values declared on a new line.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ A simple usage example:
import 'package:yaml_writer/yaml_writer.dart';
void main() {
var yamlWriter = YAMLWriter();
final yamlWriter = YamlWriter();
var yamlDoc = yamlWriter.write({
final yamlDoc = yamlWriter.write({
'name': 'Joe',
'ids': [10, 20, 30],
'desc': 'This is\na multiline\ntext',
Expand Down Expand Up @@ -75,9 +75,10 @@ Please file feature requests and bugs at the [issue tracker][tracker].

## Author

Graciliano M. Passos: [gmpassos@GitHub][github].
Graciliano M. Passos: [gmpassos@GitHub](https://github.com/gmpassos).

Liplum: [liplum@GitHub](https://github.com/liplum)

[github]: https://github.com/gmpassos

## License

Expand Down
4 changes: 2 additions & 2 deletions example/yaml_writer_example.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:yaml_writer/yaml_writer.dart';

void main() {
var yamlWriter = YamlWriter();
final yamlWriter = YamlWriter.config();

var yamlDoc = yamlWriter.write({
final yamlDoc = yamlWriter.write({
'name': 'Joe',
'ids': [10, 20, 30],
'desc': 'This is\na multiline\ntext',
Expand Down
35 changes: 35 additions & 0 deletions lib/src/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/// Style of quotes to use for string serialization.
enum QuoteStyle {
/// Example: `'value'`.
singleQuote("'"),

/// Example: `"value"`.
doubleQuote('"');

final String char;

const QuoteStyle(this.char);
}

/// Configuration for [YamlWriter].
class YamlWriterConfig {
/// The indentation size.
/// Must be greater or equal to `1`.
///
/// Defaults to `2`.
final int indentSize;

/// The quote to be used for quoting strings.
/// Defaults to [QuoteStyle.doubleQuote], because single quotes are often used as apostrophes.
final QuoteStyle quoteStyle;

/// If `true`, it will force quoting of strings.
/// If `false`, strings could be left unquoted if possible.
final bool forceQuotedString;

const YamlWriterConfig({
this.indentSize = 2,
this.quoteStyle = QuoteStyle.doubleQuote,
this.forceQuotedString = false,
});
}
113 changes: 92 additions & 21 deletions lib/src/node.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import 'dart:math';

import 'yaml_context.dart';
import 'config.dart';

class YamlContext {
final YamlWriterConfig config;

const YamlContext({
required this.config,
});
}

sealed class Node {
bool get requiresNewLine => false;
Expand Down Expand Up @@ -39,8 +47,12 @@ class StringNode extends Node {
@override
List<String> toYaml(YamlContext context) {
List<String> yamlLines = [];

if (text.contains('\n')) {
if (text.isEmpty) {
// if the text is empty, wrap with quotes.
final quote = context.config.quoteStyle.char;
yamlLines.add("$quote$quote");
} else if (text.contains('\n')) {
// if the text is multiline, use the vertical bar
bool endsWithLineBreak = text.endsWith('\n');

List<String> lines;
Expand All @@ -56,30 +68,89 @@ class StringNode extends Node {
yamlLines.add(lines[index]);
}
} else {
var containsSingleQuote = text.contains("'");

if (context.allowUnquotedStrings &&
!containsSingleQuote &&
_isValidUnquotedString(text)) {
if (!context.config.forceQuotedString && isValidUnquotedString(text)) {
// if forceQuotedString is false and the string is valid unquoted,
// don't use any quote
yamlLines.add(text);
} else if (!containsSingleQuote) {
yamlLines.add('\'$text\'');
} else {
var str = text.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
yamlLines.add('"$str"');
var result = text;
switch (context.config.quoteStyle) {
case QuoteStyle.singleQuote:
result = result.replaceAll("'", "''");
yamlLines.add("'$result'");
break;
case QuoteStyle.doubleQuote:
result = result.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
yamlLines.add('"$result"');
break;
}
}
}

return yamlLines;
}

static final _regexpInvalidUnquotedChars = RegExp(
r'[^0-9a-zA-ZàèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇßØøÅåÆæœ@/. \t-]');

bool _isValidUnquotedString(String s) =>
!_regexpInvalidUnquotedChars.hasMatch(s) &&
!s.startsWith('@') &&
!s.startsWith('-');
static final _invalidCharsRegex = RegExp(
r"^(true|false|null|~|\?|:|-)$|^\s+|\s+$|\n|\r|^[{}\[\]>,&*#|@%]|^-?(?:\d+(?:\.\d+)?|\.\d+)$|^(- |\? )|:\s*$|:\s+\S|\s+#",
);

/// ## Quoting Rules
/// ### Numbers
/// 1. Integer and decimals (including negative) are invalid.
///
/// ### at sign
/// 1. Starting with at sign is invalid.
///
/// ### Question mark
/// 1. Only a single question mark is invalid.
/// 2. Starting with question mark+whitespace is invalid.
///
/// ### Dash
/// 1. Only a single dash is invalid.
/// 2. Starting with dash+whitespace is invalid.
///
/// ### Colon
/// 1. Only a single colon is invalid.
/// 2. Colon+whitespace is invalid.
/// 3. Colon+1+whitespace+non-whitespace is invalid.
///
/// ### Brackets
/// 1. Starting with square brackets is invalid.
/// 2. Starting with curly brackets is invalid.
/// 3. Starting with angle bracket closing part is invalid.
/// 4. Starting with angle bracket closing part+dash (>-) is invalid.
///
/// ### Comma
/// 1. Starting with comma is invalid.
///
/// ### Ampersand
/// 1. Starting with ampersand is invalid.
///
/// ### Star
/// 1. Starting with star is invalid.
///
/// ### Pipe
/// 1. Starting with pipe is invalid.
/// 2. Starting with pipe+dash (|-) is invalid.
///
/// ### Hash
/// 1. Starting with hash is invalid.
/// 2. Ending with whitespace+hash is invalid.
///
/// ### Whitespace
/// 1. Starting with one or more whitespace is invalid.
/// 2. Ending with one or more whitespace is invalid.
/// 3. Containing \n, \t, \r is invalid.
///
/// ### Percentage
/// 1. Starting with percentage is invalid.
///
/// ### Builtin Literal
/// 1. Only a single tide is invalid.
/// 2. `true`, `false`, and `null` are invalid.
///
static bool isValidUnquotedString(String s) =>
!_invalidCharsRegex.hasMatch(s);
}

class ListNode extends Node {
Expand All @@ -101,7 +172,7 @@ class ListNode extends Node {
for (final node in subnodes) {
final nodeYaml = node.toYaml(context);

final firstIndent = "-${' ' * max(1, context.indentSize - 1)}";
final firstIndent = "-${' ' * max(1, context.config.indentSize - 1)}";
final subsequentIndent = ' ' * firstIndent.length;

lines.add("$firstIndent${nodeYaml.first}");
Expand Down Expand Up @@ -136,7 +207,7 @@ class MapNode extends Node {

final nodeYaml = node.toYaml(context);

final indent = ' ' * context.indentSize;
final indent = ' ' * context.config.indentSize;

if (node.requiresNewLine) {
lines.add("$key:");
Expand Down
10 changes: 0 additions & 10 deletions lib/src/yaml_context.dart

This file was deleted.

43 changes: 26 additions & 17 deletions lib/src/yaml_writer.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import 'dart:convert';
import 'dart:math';

import 'package:yaml_writer/src/node.dart';

import 'yaml_context.dart';
import 'config.dart';

@Deprecated('Use YamlWriter.')
typedef YAMLWriter = YamlWriter;
Expand All @@ -12,25 +11,36 @@ typedef YAMLWriter = YamlWriter;
class YamlWriter extends Converter<Object?, String> {
static dynamic _defaultToEncodable(dynamic object) => object.toJson();

/// The indentation size.
///
/// Must be greater or equal to `1`.
///
/// Defaults to `2`.
final int indentSize;

/// If `true` it will allow unquoted strings.
final bool allowUnquotedStrings;
final YamlWriterConfig config;

/// Used to convert objects to an encodable version.
final Object? Function(dynamic object) toEncodable;

/// Creates a [YamlWriter].
///
/// [indentSize] controls the indentation size.
///
/// If [allowUnquotedStrings] is set, strings are written without quotes if possible.
///
/// [toEncodable] is called to encode non-builtin classes.
YamlWriter({
int indentSize = 2,
this.allowUnquotedStrings = false,
Object? Function(dynamic object)? toEncodable,
}) : indentSize = max(1, indentSize),
toEncodable = toEncodable ?? _defaultToEncodable;
bool allowUnquotedStrings = false,
this.toEncodable = _defaultToEncodable,
}) : config = YamlWriterConfig(
indentSize: indentSize,
forceQuotedString: !allowUnquotedStrings,
);

/// Creates a [YamlWriter] with the given [config].
///
/// See [YamlWriterConfig].
///
/// [toEncodable] is called to encode non-builtin classes.
YamlWriter.config({
this.config = const YamlWriterConfig(),
this.toEncodable = _defaultToEncodable,
});

/// Converts [input] to an YAML document as [String].
///
Expand All @@ -44,8 +54,7 @@ class YamlWriter extends Converter<Object?, String> {
String write(Object? object) {
final node = _parseNode(object);
final context = YamlContext(
indentSize: indentSize,
allowUnquotedStrings: allowUnquotedStrings,
config: config,
);
final yaml = node.toYaml(context);
return '${yaml.join('\n')}\n';
Expand Down
3 changes: 2 additions & 1 deletion lib/yaml_writer.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// YAML Writer Library.
library yaml_writer;
library;

export 'src/yaml_writer.dart';
export "src/config.dart";
7 changes: 4 additions & 3 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name: yaml_writer
description: A library to write YAML documents, supporting Object encoding and 'dart:convert' 'Converter'.
version: 2.0.1
version: 2.1.0
homepage: https://github.com/gmpassos/yaml_writer

environment:
sdk: ">=3.0.0 <4.0.0"

dev_dependencies:
lints: ^3.0.0
lints: ^5.1.1
test: ^1.25.8
dependency_validator: ^3.2.3
dependency_validator: ^5.0.2
coverage: ^1.10.0
yaml: ^3.1.3
Loading

0 comments on commit 6b87ab4

Please sign in to comment.