Dart-YAMLファイルの解析

pubspec.yamlの中身のバージョン部分だけを変えたいなと思い、YAMLパーサーの使い方について調べ見た。

使用するパッケージは以下のものなのだけど、サンプルはあっさりしていてまた日本語系の紹介サイトもパッケージ名とかを取り出すだけなどだったので、参考にならなかった。

yaml | Dart package
A parser for YAML, a human-friendly data serialization standard

パースAPIと取得できるもの

ファイルを直接パースするのではなく、ファイルからテキスト情報をStringを取得しておき、それをパースAPIに渡して解析するという方針になっている。

そのため、File.readAsStringSync()で取り出したStringを渡すことになる。

複数ドキュメント対応のパースAPI

loadYamlStreamloadYamlDocumentsが該当する。

複数ドキュメントは、YAMLファイルの中身に「—」「…」でくくられた領域があるもの。
これら行がある場合、単一ドキュメント対応のパースAPIを実行すると例外が出るので注意する。

返り値はそれぞれYamlList、List<YamlDocument>になる。

loadYamlStreamのYamlListはList<YamlNode>とほぼ同等でList内のYamlNodeが1ドキュメントのYamlNodeになる。
対してloadYamlDocumentsはList<YamlDocument>になる。

YamlDocument.contentsがYamlNodeに該当するので、複数ドキュメントの読み込みでYamlDocumentの情報を必要とするのであればloadYamlDocumentsを使うのが良いだろう。

単一ドキュメント対応のパースAPI

loadYamlloadYamlNodeloadYamlDocumentと用意されている。

loadYamlDocumentが基本で、loadYamlNodeはloadYamlDocumentで読み込んだもののcontentsを返し、loadYamlはloadYamlNodeで読み込んだvalueを返す仕組みになっている。

Yaml~の型をわざわざ使わず、String/List/Mapと同等のアクセス方法が必要なのであれば、loadYamlを使うのが良いのだろう。

YamlNode派生として処理させたい場合はloadYamlNodeが良いと思う。

YamlNodeの処理方法

以下はloadYamlNodeで取り込んだ結果を処理したい場合の実装方法。

YamlNodeはYamlList,YamlMap,YamlScalarとして実装されており、YamlNodeというものが実体として存在することは無いようだ。

YamlNodeを受け取り、それをダウンキャストしてVisitorパターン的な処理を行うのが良いと思う。

そのVisitor的な処理を行うための基本クラスの構造は以下を参照。

import 'package:yaml/yaml.dart';

export 'package:yaml/yaml.dart';

/// [YamlNode]に対するVisotrパターンを適用するためのクラス</br>
/// YamlNode側にacceptがないのですべてこちらのクラスの中で処理するようになる。
abstract class YamlNodeHandler {
  /// キーのスタック</br>
  /// YamlMapのキーをスタックしたもの
  final stackKey = <YamlNode>[];

  /// [YamlNode]の処理
  void handle(YamlNode target) {
    if (target is YamlScalar) {
      handleScalar(target);
    } else if (target is YamlList) {
      handleList(target);
    } else if (target is YamlMap) {
      handleMap(target);
    } else {
      // YamlNodeそのもの、もしくは未対応のYamlNode派生が出た場合
      // 例外出して実装を促す。
      throw ArgumentError();
    }
  }

  /// [YamlMap]のkeyを処理する。
  void handleMapKey(YamlNode value) {
    handle(value);
  }

  /// [YamlMap]のvalueを処理する。
  void handleMapValue(YamlNode value) {
    handle(value);
  }

  /// [YamlMap]の処理</br>
  /// keyに関してはstackKeyにスタックしていく。
  void handleMap(YamlMap target) {
    target.nodes.forEach((key, value) {
      stackKey.add(key);
      handleMapKey(key);
      handleMapValue(value);
      stackKey.removeLast();
    });
  }

  /// [YamlList]の処理</br>
  /// nodesの個々の要素に対して[handle]を実行するのをsuperな処理にする。
  void handleList(YamlList target) {
    for (var value in target.nodes) {
      handle(value);
    }
  }

  /// [YamlScalar]の処理
  void handleScalar(YamlScalar target);
}

handleメソッドにYamlNodeを渡すとhandleScalar、handleList、handleMapにダウンキャストしてそれぞれ処理を行うためのメソッドを呼び出す。

後はそれぞれのメソッドで処理をすればいいのだけど、handleList、handleMapに関しては基本的な処理は入れてあり、それぞれList内の子要素に対してhandleを呼び出すのと、Map内のvalueに対してhandeを呼び出すの。
Map内のkeyに関してはstackKeyに積み上げて、今どこのMap内要素を処理しているか分かるようにはしている。

これを使うことで、読み込んだYamlNodeの全項目に対して処理が可能になる。

YamlScalarのstyle

ScalarStyleなのだけど、実際に格納されているのはANY、DOUBLE_QUOTED、FOLDED、LITERAL
PLAIN、SINGLE_QUOTEDのCONSTANTの値。

どのような値なのかはstyleでswitchして、上記CONSTANTの値でケースわけして処理する。

YamlScalarのvalue

たぶんStringだけだと思う。

YamlMap.nodesのキー部分

API仕様ではMap<dynamic, YamlNode>となっているのだけど、Map<YamlNode, YamlNode>になっていた。

YamlNode以外の文字情報

YAML形式のキーと値に関してはYamlNode内に全て納められるのだけど、それ以外の情報、例えばコメント行などについてはYamlNode内に格納されることはない。

これらを取り扱うには、パース時に渡した文字列とYamlNodeのspan(SourceSpan)を利用する必要がある。

SourceSpanはYamlNodeとして認識されたキーや文字などが、パース時に渡された文字列のどこに当たるのかを情報として保持している。

つまりspanに設定されていない文字列がYAML認識されていない文字ということになる。

サンプル

YamlNodeHandlerを取り扱い、YAMLファイルを読み込みタグやキーワード等を色付けして表示するというのを作ってみた。

これでYamlNodeの取り扱いと、それ以外の文字の取り扱いをどうするといいのかがちょっとわかると思う。

import 'dart:io';

import 'package:chalkdart/chalk.dart';

import 'yamlnodehandler.dart';

/// Yamlの印刷クラス
class PrintYaml extends YamlNodeHandler {
  PrintYaml({required this.output, required this.input});

  /// 出力先
  final IOSink output;

  /// 解析対象とするYamlの中身
  final String input;

  /// [input]に対して出力処理をしている位置
  int _next = 0;

  /// 印刷開始
  void start1() {
    _next = 0;
    final documents = loadYamlDocuments(input);
    documents.forEach((e) => handle(e.contents));
    if (_next < input.length) {
      stdout.write(input.sub string(_next));
      _next = input.length;
    }
    stdout.write("\n");
  }

  void start2() {
    _next = 0;
    handle(loadYamlStream(input));
    if (_next < input.length) {
      stdout.write(input.sub string(_next));
      _next = input.length;
    }
    stdout.write("\n");
  }

  @override
  void handleList(YamlList target) {
    _writeNext(target.span.start.offset);
    super.handleList(target);
    _writeNext(target.span.end.offset);
    _next = target.span.end.offset + 1;
  }

  @override
  void handleScalar(YamlScalar target) {
    _writeNext(target.span.start.offset);
    if (target.value != null) {
      // target.valueはString前提で処理をしている
      _writeQuoted(target.style);
      output.write(chalk.white(target.value));
      _writeQuoted(target.style);
    }
    _next = target.span.end.offset;
  }

  /// [_next]から[next]までのデータを出力する
  void _writeNext(int next) {
    output.write(input.sub string(_next, next));
    _next = next;
  }

  /// Quote文字の出力
  void _writeQuoted(ScalarStyle style) {
    switch (style) {
      case ScalarStyle.DOUBLE_QUOTED:
        output.write(chalk.yellow('"'));
        break;
      case ScalarStyle.SINGLE_QUOTED:
        output.write(chalk.yellow("'"));
        break;
    }
  }
}

const yamlFile = 'yaml/test4.yaml';

void main() {
  final printer =
      PrintYaml(output: stdout, input: File(yamlFile).readAsStringSync());
  printer.start1();
  print("");
  printer.start2();
}

  • コード中「sub string」と途中にスペースが入っているけど実際動かす場合はスペースをとって。
    スペースを取った形で書くとページが保存できなかったので。

コメント

タイトルとURLをコピーしました