Files
FlClash/lib/features/editor/relaxed_yaml.dart
T
chen08209 9bd2286eb5 feat(ui): layer surfaces with fills and follow the iOS type scale
Changelog: iOS-style surfaces, alerts and sheets; clearer TV focus
2026-10-17 17:21:43 +08:00

281 lines
7.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:math';
import 'package:collection/collection.dart';
import 'clash_schema.dart';
/// Runs [read] on [content] and, when that throws, once more on
/// [relaxYaml]'s rewrite of it; the error of the text as written stands when
/// the rewrite fails too.
T readRelaxed<T>(
String content,
EditorSchema schema,
T Function(String content) read,
) {
try {
return read(content);
} catch (_) {
final relaxed = relaxYaml(content, schema);
if (relaxed != null && relaxed != content) {
try {
return read(relaxed);
} catch (_) {
// The rewrite's own error would point at lines the user never wrote.
}
}
rethrow;
}
}
/// Nests each key [schema] knows under the map that holds it whatever its
/// indentation, drops a `dns:`-style header around the whole document, and
/// turns a list written on one line, comma separated, into block entries.
/// Null when [schema] is not a map.
String? relaxYaml(String content, EditorSchema schema) {
final root = schema.root;
if (root.kind != YamlKind.map) {
return null;
}
return _Relaxer(root, schema.section).run(content);
}
final _namedKeyPattern = RegExp(r'^([A-Za-z][A-Za-z0-9-]*)\s*[::](\s*)(.*)$');
final _freeKeyPattern = RegExp(
r'''^('[^']*'|"[^"]*"|[^\s#'"{\[\-][^#]*?)\s*:(?:\s+(.*))?$''',
);
final _nodeStart = RegExp(r'^[\[{|>]');
final _plainUnsafeStart = RegExp(r'''^[-?:,\[\]{}#&*!|>%@`'"]''');
final _listSeparator = RegExp('[,,]');
const _opaque = YamlSchema.map({});
class _Frame {
final YamlSchema schema;
final int indent;
final int ownerIndent;
int? childIndent;
_Frame(this.schema, this.indent, this.ownerIndent);
MapEntry<String, YamlSchema>? field(String name) {
final lower = name.toLowerCase();
return schema
.fieldsFor(null)
.entries
.firstWhereOrNull((entry) => entry.key.toLowerCase() == lower);
}
}
class _Block {
final int indent;
final bool isList;
int? base;
_Block(this.indent, {required this.isList});
}
class _Relaxer {
final List<_Frame> _frames;
final RegExp? _header;
final _out = <String>[];
_Block? _block;
var _started = false;
_Relaxer(YamlSchema root, String? section)
: _frames = [_Frame(root, 0, -1)],
_header = section == null
? null
: RegExp(
'^${RegExp.escape(section)}\\s*[::]\\s*\$',
caseSensitive: false,
);
String run(String content) {
for (final raw in content.split('\n')) {
final (indent, text) = _split(raw);
if (text.isEmpty || text.startsWith('#')) {
continue;
}
if (!_started && (_header?.hasMatch(text) ?? false)) {
continue;
}
_line(indent, text);
}
return '${_out.join('\n')}\n';
}
static (int, String) _split(String raw) {
var indent = 0;
var index = 0;
for (; index < raw.length; index++) {
final width = switch (raw[index]) {
' ' || '\u00a0' => 1,
'\t' || '\u3000' => 2,
_ => 0,
};
if (width == 0) {
break;
}
indent += width;
}
return (indent, raw.substring(index).trimRight());
}
void _line(int indent, String text) {
if (!_started) {
final item = _itemOf(text);
if (item != null &&
_keyLine(indent + text.length - item.length, item, knownOnly: true)) {
return;
}
}
if (!_keyLine(indent, text)) {
_valueLine(indent, text);
}
}
/// A key the schema does not know still nests by its indentation.
bool _keyLine(int indent, String text, {bool knownOnly = false}) {
final named = _namedKeyPattern.firstMatch(text);
if (named != null) {
final rest = named[3]!;
final spaced = named[2]!.isNotEmpty || rest.isEmpty;
final known = [
for (final (index, frame) in _frames.indexed)
if (frame.field(named[1]!) != null) index,
];
if (known.isNotEmpty && (spaced || !rest.contains(':'))) {
final frame = _enter(_pick(known, indent), indent);
final field = frame.field(named[1]!)!;
_emitKey(frame, field.key, field.value, rest, indent);
return true;
}
}
final free = knownOnly ? null : _freeKeyPattern.firstMatch(text);
if (free == null) {
return false;
}
final all = List.generate(_frames.length, (index) => index);
final frame = _enter(_pick(all, indent), indent);
final schema = frame.schema.entry ?? _opaque;
_emitKey(frame, free[1]!, schema, free[2] ?? '', indent);
return true;
}
/// The innermost of [candidates] whose keys sit at [indent], then the
/// innermost [indent] could still be inside.
int _pick(List<int> candidates, int indent) {
bool within(int index) {
final frame = _frames[index];
final childIndent = frame.childIndent;
return childIndent == null
? indent > frame.ownerIndent
: childIndent <= indent;
}
return candidates.lastWhereOrNull(
(index) => _frames[index].childIndent == indent,
) ??
candidates.lastWhereOrNull(within) ??
candidates.first;
}
_Frame _enter(int index, int indent) {
_frames.removeRange(index + 1, _frames.length);
_started = true;
return _frames[index]..childIndent ??= indent;
}
void _emitKey(
_Frame frame,
String key,
YamlSchema schema,
String value,
int indent,
) {
final pad = ' ' * frame.indent;
final inline = _stripComment(value);
final isList = _isScalarList(schema);
if (inline.isNotEmpty && (!isList || _nodeStart.hasMatch(inline))) {
_block = _Block(frame.indent + 2, isList: false);
_out.add('$pad$key: $value');
return;
}
_block = _Block(frame.indent + 2, isList: isList);
_out.add('$pad$key:');
if (inline.isNotEmpty) {
_entries(inline);
} else if (schema.kind == YamlKind.map) {
_frames.add(_Frame(schema, frame.indent + 2, indent));
}
}
void _valueLine(int indent, String text) {
final block = _block;
if (block == null) {
_out.add('${' ' * indent}$text');
return;
}
if (block.isList) {
final item = _itemOf(text, loose: true);
if (item != null) {
final entry = _stripComment(item);
final pad = ' ' * block.indent;
_out.add(entry.isEmpty ? '$pad-' : '$pad- ${_scalar(entry)}');
return;
}
if (!_nodeStart.hasMatch(text)) {
_entries(_stripComment(text));
return;
}
}
final base = block.base ??= indent;
_out.add('${' ' * (block.indent + max(0, indent - base))}$text');
}
void _entries(String text) {
final pad = ' ' * _block!.indent;
for (final part in text.split(_listSeparator)) {
final entry = part.trim();
if (entry.isNotEmpty) {
_out.add('$pad- ${_scalar(entry)}');
}
}
}
static bool _isScalarList(YamlSchema schema) =>
schema.kind == YamlKind.list && schema.entry!.kind == YamlKind.scalar;
/// [loose] also takes `-entry`, which YAML reads as a plain scalar.
static String? _itemOf(String text, {bool loose = false}) {
if (!text.startsWith('-')) {
return null;
}
final rest = text.substring(1);
if (rest.isEmpty || rest.trimLeft() != rest) {
return rest.trimLeft();
}
return loose && !rest.startsWith('-') ? rest : null;
}
static bool _isQuote(String text) =>
text.startsWith("'") || text.startsWith('"');
static String _stripComment(String text) {
final from = _isQuote(text) ? max(text.indexOf(text[0], 1), 0) : 0;
final index = text.startsWith('#') ? 0 : text.indexOf(' #', from);
return index < 0 ? text : text.substring(0, index).trimRight();
}
static String _scalar(String text) {
if (text.length > 1 && _isQuote(text) && text.endsWith(text[0])) {
return text;
}
final plain =
!_plainUnsafeStart.hasMatch(text) &&
!text.contains(': ') &&
!text.contains(' #') &&
!text.endsWith(':');
return plain ? text : "'${text.replaceAll("'", "''")}'";
}
}