From 4957ea7a2e10dcbe026e1492fda34f8e153f7e5c Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 11 Mar 2026 00:03:04 +0800 Subject: [PATCH 01/11] Split and rewrite coverage on converters and codecs --- examples/libraries/dart_test.yaml | 1 + examples/libraries/pubspec.yaml | 10 + examples/pubspec.yaml | 1 + firebase.json | 1 + .../libraries/convert/build-custom-codecs.md | 682 +++++++++++++++ .../convert/converters-and-codecs.md | 805 +++++++----------- src/content/libraries/convert/index.md | 12 + src/data/sidenav/default.yml | 7 + 8 files changed, 1001 insertions(+), 518 deletions(-) create mode 100644 examples/libraries/dart_test.yaml create mode 100644 examples/libraries/pubspec.yaml create mode 100644 src/content/libraries/convert/build-custom-codecs.md create mode 100644 src/content/libraries/convert/index.md diff --git a/examples/libraries/dart_test.yaml b/examples/libraries/dart_test.yaml new file mode 100644 index 0000000000..36f32067b0 --- /dev/null +++ b/examples/libraries/dart_test.yaml @@ -0,0 +1 @@ +include: ../dart_test_base_browser.yaml diff --git a/examples/libraries/pubspec.yaml b/examples/libraries/pubspec.yaml new file mode 100644 index 0000000000..1567f4797d --- /dev/null +++ b/examples/libraries/pubspec.yaml @@ -0,0 +1,10 @@ +name: libraries +description: Analyzed source of code snippets covering Dart libraries. +publish_to: none + +resolution: workspace +environment: + sdk: ^3.11.0 + +dev_dependencies: + test: ^1.28.0 diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index 2abceaf1d5..7581c731f5 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -19,6 +19,7 @@ workspace: - html - iterables - language + - libraries - misc - non_promotion - type_system diff --git a/firebase.json b/firebase.json index d4f6209d81..825812a86c 100644 --- a/firebase.json +++ b/firebase.json @@ -356,6 +356,7 @@ { "source": "/language/specification", "destination": "/resources/language/spec", "type": 301 }, { "source": "/language/versions", "destination": "/language/versioning", "type": 301 }, { "source": "/libraries/async", "destination": "/libraries/dart-async", "type": 301 }, + { "source": "/libraries/convert", "destination": "/libraries/dart-convert", "type": 301 }, { "source": "/libraries/serialization", "destination": "/libraries/serialization/json", "type": 301 }, { "source": "/linter/lints/:lint*", "destination": "/tools/linter-rules/:lint", "type": 301 }, { "source": "/lints", "destination": "/tools/linter-rules", "type": 301 }, diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md new file mode 100644 index 0000000000..5cf750bcae --- /dev/null +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -0,0 +1,682 @@ +--- +title: Build custom codecs and converters +description: >- + Learn how to implement your own Codec and Converter classes + with support for streaming and composition. +--- + +The [`dart:convert`][] library defines [`Codec`][] and [`Converter`][] +as extensible base classes. By implementing these classes, +you give your conversion logic a standard interface +that neatly integrates with Dart's stream system +and composes with other codecs through [`fuse`][]. + +To illustrate the process of implementing a custom codec from scratch, +this guide walks through implementing a [Caesar cipher][], +starting with a converter and progressively adding stream support. +A _Caesar cipher_ is a relatively simple encryption scheme that +shifts each letter by a fixed number of positions in the alphabet. + +:::tip +To learn more about codecs and converters and the built-in ones, +check out [Converters and codecs][]. +::: + +[`dart:convert`]: {{site.dart-api}}/dart-convert +[`Converter`]: {{site.dart-api}}/dart-convert/Converter-class.html +[`Codec`]: {{site.dart-api}}/dart-convert/Codec-class.html +[`fuse`]: {{site.dart-api}}/dart-convert/Codec/fuse.html + +[Caesar cipher]: https://wikipedia.org/wiki/Caesar_cipher + +[Converters and codecs]: /libraries/convert/converters-and-codecs + +## When to build a custom codec + +Build a custom `Codec` or `Converter` when your conversion: + +- Is **bidirectional** and you need both encoding and decoding. +- Benefits from **streaming** because the data might be + too large for a single in-memory transformation. +- Should **compose** with other codecs through `fuse`. +- Is meant to be **reused** across projects or published as a package. + +For one-directional or internal conversions, +a purpose-built function is often simpler. +For application-level JSON serialization, +the `fromJson`/`toJson` convention with code generation is more idiomatic. +For more insight on when to use each approach, +check out [Codecs versus `fromJson` and `toJson`][when-to-use-codecs]. + +[when-to-use-codecs]: /libraries/convert/converters-and-codecs#when-to-use-codecs + +## Build a basic converter + +Start by extending `Converter` and implementing the [`convert`][] method. +This is the minimum required to create a working converter. + +The following [Caesar cipher][] encoder shifts each +lowercase letter forward in the alphabet by a given amount. +Non-letter characters pass through unchanged: + +```dart +import 'dart:convert'; + +/// Encodes a string by shifting each letter forward in the alphabet. +class CaesarEncoder extends Converter { + final int shift; + + const CaesarEncoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, shift)); + } + return buffer.toString(); + } +} + +/// Shifts the specified [codeUnit] by [shift] positions in the alphabet. +/// +/// Only shifts lowercase ASCII letters (a-z). +/// All other characters are returned unchanged. +int _shiftCodeUnit(int codeUnit, int shift) { + const a = 0x61; + const z = 0x7A; + if (codeUnit >= a && codeUnit <= z) { + return a + (codeUnit - a + shift) % 26; + } + return codeUnit; +} +``` + +You can now use the converter to encode strings: + +```dart +void main() { + const encoder = CaesarEncoder(3); + print(encoder.convert('hello')); // khoor + print(encoder.convert('xyz')); // abc +} +``` + +[`convert`]: {{site.dart-api}}/dart-convert/Converter/convert.html + +## Build the decoder + +The inverse of a Caesar encoder with a shift of `N` +is a Caesar encoder with a shift of `26 - N`. +You could reuse `CaesarEncoder` directly, +but creating a separate class keeps the intent clear and +lets you customize behavior if needed: + +```dart +/// Decodes a Caesar-cipher-encoded string by +/// shifting each letter backward in the alphabet. +class CaesarDecoder extends Converter { + final int shift; + + const CaesarDecoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + // Shift backward by shifting forward by (26 - shift). + buffer.writeCharCode(_shiftCodeUnit(codeUnit, 26 - shift)); + } + return buffer.toString(); + } +} +``` + +## Wrap converters in a codec + +A `Codec` pairs an encoder (`Converter`) +with a decoder (`Converter`). +Extend `Codec` and override the [`encoder`][] and [`decoder`][] getters: + +```dart +/// A codec that encodes and decodes string using a +/// [Caesar cipher](https://wikipedia.org/wiki/Caesar_cipher). +class CaesarCodec extends Codec { + @override + final CaesarEncoder encoder; + + @override + final CaesarDecoder decoder; + + const CaesarCodec(int shift) + : encoder = CaesarEncoder(shift), + decoder = CaesarDecoder(shift); + + /// Creates a [CaesarCodec] that uses ROT13 encoding. + const CaesarCodec.rot13() : this(13); +} +``` + +The codec inherits the `encode` and `decode` methods from `Codec` +that delegate to the overriden encoder and decoder. +It also inherits the `fuse` method and `inverted` getter: + +```dart +void main() { + const caesar = CaesarCodec(3); + + final encoded = caesar.encode('hello'); + print(encoded); // khoor + + final decoded = caesar.decode(encoded); + print(decoded); // hello + + // The `inverted` getter returns a new codec that + // applies converts in the inverse direction of the codec. + final inverted = caesar.inverted; + print(inverted.encode('khoor')); // hello +} +``` + +At this point you have a fully functional codec. +The following sections add stream support, +enabling consumers to use the codec with the [`Stream.transform`][] method. + +[`encoder`]: {{site.dart-api}}/dart-convert/Codec/encoder.html +[`decoder`]: {{site.dart-api}}/dart-convert/Codec/decoder.html +[`Stream.transform`]: {{site.dart-api}}/dart-async/Stream/transform.html + +## Add chunked conversion for streams + +The basic `convert` method processes all input at once. +To support streaming, override the +`startChunkedConversion` method in your converter. +This method receives a downstream output sink and +returns an upstream input sink. +The chunked conversion then follows this sequence: + +1. The caller passes an output sink + to `startChunkedConversion` on the converter. + The converter creates and returns an input sink. +1. The caller calls `inputSink.add(data)` + one or more times with chunks of input. + Each call converts the data and + forwards the result to the output sink. +1. The caller calls `inputSink.close()`. + The input sink sends any remaining converted data + to the output sink, then closes it. + +To implement this in your converter, start by +creating a sink class that performs the conversion: + +```dart +/// A [StringConversionSink] that applies a Caesar cipher [_shift] and +/// forwards the result to the [_output] sink. +class _CaesarEncoderSink extends StringConversionSinkBase { + final int _shift; + final StringConversionSink _output; + + _CaesarEncoderSink(this._shift, this._output); + + @override + void addSlice(String chunk, int start, int end, bool isLast) { + final buffer = StringBuffer(); + for (var i = start; i < end; i++) { + buffer.writeCharCode( + _shiftCodeUnit(chunk.codeUnitAt(i), _shift), + ); + } + _output.add(buffer.toString()); + if (isLast) { + _output.close(); + } + } +} +``` + +The `addSlice` method receives a chunk of the input string along with +`start` and `end` indices that define the relevant portion of the chunk. +The `isLast` flag indicates whether this is the final chunk. +When `isLast` is `true`, the sink closes the output without +requiring a separate `close` call. + +:::note +`StringConversionSink` is a base class that +implements `add`, `addSlice`, and `close` in terms of each other. +You only need to override `addSlice()`. +For byte-oriented converters, use `ByteConversionSink` instead. +::: + +Next, override `startChunkedConversion` in your encoder +to return an instance of your sink: + +```dart +class CaesarEncoder extends Converter { + final int shift; + + const CaesarEncoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, shift)); + } + return buffer.toString(); + } + + @override + StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(shift, stringSink); + } +} +``` + +Apply the same approach to `CaesarDecoder`, +using `26 - shift` as the shift value in its sink. + +With chunked conversion implemented, +the converter automatically works as a `StreamTransformer` +through the inherited `bind` method. + +### Chunked conversion type versus synchronous type + +The chunked (streaming) type signature of a converter +doesn't always match the synchronous `convert` signature. +For example, `LineSplitter` synchronously converts +all lines in a `String` to a `List` at once, +but in chunked mode it converts `String` chunks to `String` chunks +where each output chunk is a single line. +The chunked type is determined by +what's most useful as a `StreamTransformer`. + +## Use your codec with streams + +Once your converters support chunked conversion, +you can use them with the [`transform`][] method on `Stream`: + +```dart +import 'dart:convert'; +import 'dart:io'; + +void main() async { + const caesar = CaesarCodec(3); + + // Encrypt a file as a stream. + final encrypted = File('message.txt') + .openRead() + .transform(utf8.decoder) + .transform(caesar.encoder); + + await for (final chunk in encrypted) { + stdout.write(chunk); + } +} +``` + +You can also compose your codec with others using [`fuse`][codec-fuse] +to build data pipelines: + +```dart +import 'dart:convert'; +import 'dart:io'; + +void main() async { + const caesar = CaesarCodec(3); + + // Create a codec that encrypts, then compresses. + final encryptAndCompress = caesar.fuse(utf8).fuse(gzip); + + // Write encrypted, compressed data. + final output = File('message.gz').openWrite(); + output.add(encryptAndCompress.encode('Secret message.')); + await output.close(); + + // Read and decrypt. + final bytes = await File('message.gz').readAsBytes(); + final decrypted = encryptAndCompress.decode(bytes); + print(decrypted); // Secret message. +} +``` + +[`transform`]: {{site.dart-api}}/dart-async/Stream/transform.html +[codec-fuse]: {{site.dart-api}}/dart-convert/Codec/fuse.html + +## Test your codec + +Codecs have several properties that make good test cases. +At a minimum, verify that encoding and decoding are inverses, +that edge cases are handled, and that chunked conversion produces +the same results as single-pass conversion. + +To test chunked conversion without setting up a stream, +you can use the `ChunkedConversionSink.withCallback` factory constructor: + +```dart +import 'dart:async'; +import 'dart:convert'; + +import 'package:test/test.dart'; + +void main() { + const codec = CaesarCodec(3); + + group('CaesarCodec', () { + test('encode shifts letters forward', () { + expect(codec.encode('abc'), equals('def')); + expect(codec.encode('xyz'), equals('abc')); + }); + + test('decode reverses encode', () { + const original = 'the quick brown fox'; + final encoded = codec.encode(original); + expect(codec.decode(encoded), equals(original)); + }); + + test('non-letter characters pass through unchanged', () { + expect(codec.encode('hello, world!'), equals('khoor, zruog!')); + }); + + test('empty string encodes to empty string', () { + expect(codec.encode(''), equals('')); + }); + + test('inverted codec swaps encode and decode', () { + final inverted = codec.inverted; + expect(inverted.encode('def'), equals('abc')); + expect(inverted.decode('abc'), equals('def')); + }); + + test('chunked conversion matches single-pass conversion', () { + final chunks = []; + final outputSink = ChunkedConversionSink.withCallback( + (accumulated) => chunks.addAll(accumulated), + ); + + // Manually drive a chunked conversion. + final inputSink = codec.encoder.startChunkedConversion( + outputSink, + ); + inputSink.add('hel'); + inputSink.add('lo '); + inputSink.add('world'); + inputSink.close(); + + expect(chunks.join(), equals(codec.encode('hello world'))); + }); + + test('chunked conversion works with streams', () async { + const input = 'hello world'; + + // Split the input into chunks and convert as a stream. + final stream = Stream.fromIterable(['hel', 'lo ', 'world']); + final result = await stream + .transform(codec.encoder) + .join(); + + expect(result, equals(codec.encode(input))); + }); + }); +} +``` + +## Complete example + +The following example brings together all the pieces from this guide, +implementing a complete Caesar cipher codec: + +```dart +import 'dart:convert'; + +/// Shifts the specified [codeUnit] by [shift] positions in the alphabet. +/// +/// Only shifts lowercase ASCII letters (a-z). +/// All other characters are returned unchanged. +int _shiftCodeUnit(int codeUnit, int shift) { + const a = 0x61; + const z = 0x7A; + if (codeUnit >= a && codeUnit <= z) { + return a + (codeUnit - a + shift) % 26; + } + return codeUnit; +} + +/// A [StringConversionSink] that applies a Caesar cipher shift +/// and forwards the result to [_output]. +class _CaesarEncoderSink extends StringConversionSinkBase { + final int _shift; + final StringConversionSink _output; + + _CaesarEncoderSink(this._shift, this._output); + + @override + void addSlice(String chunk, int start, int end, bool isLast) { + final buffer = StringBuffer(); + for (var i = start; i < end; i++) { + buffer.writeCharCode( + _shiftCodeUnit(chunk.codeUnitAt(i), _shift), + ); + } + _output.add(buffer.toString()); + if (isLast) { + _output.close(); + } + } +} + +/// Encodes a string by shifting each letter forward in the alphabet. +class CaesarEncoder extends Converter { + final int shift; + + const CaesarEncoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, shift)); + } + return buffer.toString(); + } + + @override + StringConversionSink startChunkedConversion(Sink sink) { + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(shift, stringSink); + } +} + +/// Decodes a Caesar-cipher-encoded string +/// by shifting each letter backward in the alphabet. +class CaesarDecoder extends Converter { + final int shift; + + const CaesarDecoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, 26 - shift)); + } + return buffer.toString(); + } + + @override + StringConversionSink startChunkedConversion(Sink sink) { + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(26 - shift, stringSink); + } +} + +/// A codec that encodes and decodes strings using a +/// [Caesar cipher](https://en.wikipedia.org/wiki/Caesar_cipher). +class CaesarCodec extends Codec { + @override + final CaesarEncoder encoder; + + @override + final CaesarDecoder decoder; + + const CaesarCodec(int shift) + : encoder = CaesarEncoder(shift), + decoder = CaesarDecoder(shift); + + /// Creates a [CaesarCodec] that uses ROT13 encoding. + const CaesarCodec.rot13() : this(13); +} + +void main() { + const codec = CaesarCodec(3); + + final encoded = codec.encode('the quick brown fox'); + print(encoded); // wkh txlfn eurzq ira + + final decoded = codec.decode(encoded); + print(decoded); // the quick brown fox +} +``` + +## Design considerations + +When designing your own codecs and converters, +keep the following best practices in mind. + +### Make codecs const-constructable when possible + +If your codec's configuration is able to be immutable, +specify its constructors as `const`. +This lets callers create compile-time constant instances +and follows the pattern set by the built-in codecs. + +### Add named parameters for configurable codecs + +If a codec supports configuration options, +add named parameters to both the constructor (for defaults) +and the `encode` and `decode` methods (for per-call overrides). +`JsonCodec` demonstrates this pattern, +where its constructor accepts a default `reviver`, +and its `decode` method accepts one that overrides it per call: + +```dart +class JsonCodec extends Codec { + // ... + + // Constructor sets defaults. + const JsonCodec({this.reviver, this.toEncodable}); + + // Method overrides accept per-call options. + + @override + dynamic decode(String source, { + Object? Function(Object?, Object?)? reviver, + }) { /* ... */ } + + @override + String encode(Object? value, { + Object? Function(dynamic)? toEncodable, + }) { /* ... */ } + + // ... +} +``` + +### Override fuse for optimized composition + +If your codec is commonly composed with another specific codec, +override the `fuse` method to return an optimized implementation. +The built-in `JsonCodec` does this: when fused with `Utf8Codec`, +it returns a codec that uses [`JsonUtf8Encoder`][] to +bypass the intermediate string representation. + +[`JsonUtf8Encoder`]: {{site.dart-api}}/dart-convert/JsonUtf8Encoder-class.html + +### Choose the right sink base class + +Dart provides specialized sink base classes that can +improve performance by avoiding unnecessary conversions: + +[`StringConversionSink`][] +: For converters that input or output strings. + Provides an `addSlice` method with start and end indices + to avoid substring allocation. + +[`ByteConversionSink`][] +: For converters that input or output byte lists. + Provides an `addSlice` method with start and end indices + to avoid copying byte ranges. + +[`ChunkedConversionSink`][] +: The general-purpose base class. + Use this when your data is neither strings nor bytes. + +:::important +Custom chunked conversion sinks should prefer extending or mixing-in +the corresponding base class rather than implementing the interface directly. +This helps your sink continue to work correctly if +new members are added to super types in the future. +::: + +### Respect data ownership in sinks + +Data passed to a sink's `add` method shouldn't be +modified by the caller afterward. +Sinks are allowed to hold references to the data +rather than copying it immediately. +Since they're immutable, this is naturally safe for strings, +but for `List` byte data, the caller must +either pass a fresh list or avoid reusing it. + +The `addSlice` method on `ByteConversionSink` relaxes this restriction because +it accepts `start` and `end` indices with a copy-on-need contract. +Therefore, the caller can reuse the underlying list after the call returns. +This is one of the key performance advantages of the specialized sink classes. + +### Handle state across chunks carefully + +Some conversions carry state between chunks. +For example, a UTF-8 decoder might receive a chunk +that ends in the middle of a multibyte character. +If your conversion has this property, +store intermediate state in your sink class +and finalize it in the `close` method. + +A safe but inefficient fallback for complex conversions is to +buffer all incoming chunks and perform the entire conversion in `close`. +This trades streaming efficiency for implementation simplicity and +might be an appropriate starting point before optimizing. + +### Provide top-level instances + +Following the convention of the built-in codecs, consider +exposing commonly used configurations as `const` top-level instances: + +```dart +/// A [CaesarCodec] with the standard shift of 13 (ROT13). +const rot13 = CaesarCodec.rot13(); +``` + +This makes the codec easy to discover and use. + +## What's next + +- For detailed documentation on all built-in codecs and converters, + browse the [`dart:convert` API docs][library-api-docs]. +- For examples of well-implemented codecs and converters, + such as `HexCodec` and its `HexEncoder` and `HexDecoder`, + reference the implementation of [`package:convert`][]. +- If you haven't yet, read the [Converters and codecs][] for + an introduction to using the built-in codecs and converters. + +[library-api-docs]: {{site.dart-api}}/dart-convert +[pkg-convert]: {{site.pub-pkg}}/convert +[Converters and codecs]: /libraries/convert/converters-and-codecs diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index c2eb7befc3..13588de9de 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -1,615 +1,384 @@ --- title: Converters and codecs -description: Learn how to write efficient conversions. -showBreadcrumbs: false -original-date: 2014-02-06 -date: 2015-03-17 -obsolete: true +description: >- + Learn how to use Dart's codec and converter classes + to encode, decode, and transform data. --- -_Written by Florian Loitsch
-February 2014 (updated March 2015)_ - -Converting data between different representations is a common task in computer -engineering. Dart is no exception and comes with -[dart:convert]({{site.dart-api}}/dart-convert/dart-convert-library.html), a -core library that provides a set of converters -and useful tools to build new converters. -Examples of converters provided by the library include those -for commonly used encodings such as JSON and UTF-8. -In this document, we show how Dart's -converters work and how you can create your own efficient converters -that fit into the Dart world. - -## Big picture - -Dart's conversion architecture is -based on _converters_, which translate from one representation to another. -When conversions are reversible, two converters are grouped together into a -_codec_ (coder-decoder). The term codec is frequently used for audio and -video processing but also applies to string encodings such as UTF-8 or JSON. - - -By convention, all converters in Dart use the abstractions provided in the -dart:convert library. This provides a consistent API for developers and ensures -that converters can work together. -For instance, converters (or codecs) can be fused together if their -type matches, and the resulting converter can then be used as a single unit. -Furthermore, these fused converters frequently work more efficiently than if -they had been used separately. - -## Codec - -A codec is a combination of two converters where one encodes -and the other one decodes: +The [`dart:convert`][] library provides a framework for +encoding, decoding, and transforming data in Dart. +At its core are two abstractions: +**converters** that transform data from one representation to another and +**codecs** that group two inverse converters together. + +You already use this system every time you call [`json.decode`][] or +read a text file with [`Utf8Decoder`][] retrieved using [`utf8.decoder`][]. +This guide explains the concepts behind these tools +and shows you how to get more out of them. + +[`dart:convert`]: {{site.dart-api}}/dart-convert +[`json.decode`]: {{site.dart-api}}/dart-convert/JsonCodec/decode.html +[`Utf8Decoder`]: {{site.dart-api}}/dart-convert/Utf8Decoder-class.html +[`utf8.decoder`]: {{site.dart-api}}/dart-convert/Utf8Codec/decoder.html + +## What codecs and converters are + +[`Converter`][] +: Transforms data from one type to another. + Every converter extends `Converter`, + where `S` is the input type and `T` is the output type. + For example, `JsonEncoder` converts a Dart object to a JSON string, + and `Utf8Encoder` converts a string to UTF-8 bytes. + +[`Codec`][] +: Groups two converters that are inverses of each other: + an encoder and a decoder. + Every codec extends `Codec`, + where encoding goes from `S` to `T` + and decoding goes from `T` back to `S`. + For example, `JsonCodec` pairs `JsonEncoder` with `JsonDecoder`. + +Converters also implement [`StreamTransformer`][]. +As a result, you can use any converter directly with +the [`Stream.transform`][] instance method to process data incrementally, +without loading everything into memory at once. + +[`Converter`]: {{site.dart-api}}/dart-convert/Converter-class.html +[`Codec`]: {{site.dart-api}}/dart-convert/Codec-class.html +[`StreamTransformer`]: {{site.dart-api}}/dart-async/StreamTransformer-class.html +[`Stream.transform`]: {{site.dart-api}}/dart-async/Stream/transform.html + +## Built-in codecs + +Besides JSON and UTF-8, the `dart:convert` library provides +codecs for several common formats. +It also provides top-level variables with +default-configured codec instances for convenience: + +| Codec class | Encodes from | Encodes to | Default instance | +|-------------------|--------------|-----------------|------------------| +| [`JsonCodec`][] | Dart objects | JSON string | [`json`][] | +| [`Utf8Codec`][] | `String` | UTF-8 bytes | [`utf8`][] | +| [`AsciiCodec`][] | `String` | ASCII bytes | [`ascii`][] | +| [`Latin1Codec`][] | `String` | Latin-1 bytes | [`latin1`][] | +| [`Base64Codec`][] | Bytes | Base64 `String` | [`base64`][] | + +The `dart:io` library additionally provides a few compression codecs: + +| Codec class | Encodes from | Encodes to | Default instance | +|-----------------|--------------|-----------------------|------------------| +| [`GZipCodec`][] | Bytes | GZip-compressed bytes | [`gzip`][] | +| [`ZLibCodec`][] | Bytes | ZLib-compressed bytes | [`zlib`][] | + +Besides the provided codecs and their converters, +`dart:convert` also provides the [`HtmlEscape`][] converter, +a converter that escapes HTML special characters, and [`LineSplitter`][], +a stream transformer that splits strings into individual lines. + +[`JsonCodec`]: {{site.dart-api}}/dart-convert/JsonCodec-class.html +[`json`]: {{site.dart-api}}/dart-convert/json.html +[`Utf8Codec`]: {{site.dart-api}}/dart-convert/Utf8Codec-class.html +[`utf8`]: {{site.dart-api}}/dart-convert/utf8.html +[`AsciiCodec`]: {{site.dart-api}}/dart-convert/AsciiCodec-class.html +[`ascii`]: {{site.dart-api}}/dart-convert/ascii.html +[`Latin1Codec`]: {{site.dart-api}}/dart-convert/Latin1Codec-class.html +[`latin1`]: {{site.dart-api}}/dart-convert/latin1.html +[`Base64Codec`]: {{site.dart-api}}/dart-convert/Base64Codec-class.html +[`base64`]: {{site.dart-api}}/dart-convert/base64.html + +[`GZipCodec`]: {{site.dart-api}}/dart-io/GZipCodec-class.html +[`gzip`]: {{site.dart-api}}/dart-io/gzip.html +[`ZLibCodec`]: {{site.dart-api}}/dart-io/ZLibCodec-class.html +[`zlib`]: {{site.dart-api}}/dart-io/zlib.html + +[`HtmlEscape`]: {{site.dart-api}}/dart-convert/HtmlEscape-class.html +[`LineSplitter`]: {{site.dart-api}}/dart-convert/LineSplitter-class.html + +## Built-in converters + +## Encode and decode data + +Every codec provides [`encode`][] and [`decode`][] convenience methods. +These delegate to the codec's [`encoder`][] and [`decoder`][] converters: ```dart -abstract class Codec { - const Codec(); - - T encode(S input) => encoder.convert(input); - S decode(T encoded) => decoder.convert(encoded); +import 'dart:convert'; - Converter get encoder; - Converter get decoder; +void main() { + // Encode a Dart object to a JSON string. + final jsonString = json.encode({'name': 'Dash', 'age': 5}); + print(jsonString); // {"name":"Dash","age":5} - Codec fuse(Codec other) { .. } - Codec get inverted => ...; + // Decode a JSON string back to a Dart object. + final data = json.decode(jsonString) as Map; + print(data['name']); // Dash } ``` -As can be seen, codecs provide convenience methods such as `encode()` and -`decode()` that are expressed in terms of the encoder and decoder. The `fuse()` -method and `inverted` getter allow you to fuse converters and -change the direction of a codec, respectively. -The base implementation of -[Codec]({{site.dart-api}}/dart-convert/Codec-class.html) -for these two members -provides a solid default implementation -and implementers usually don't need to worry about them. - -The `encode()` and `decode()` -methods, too, may be left untouched, but they can be extended for additional -arguments. For example, the -[JsonCodec]({{site.dart-api}}/dart-convert/JsonCodec-class.html) -adds named arguments to `encode()` and `decode()` -to make these methods more useful: - -```dart -dynamic decode(String source, {reviver(var key, var value)}) { … } -String encode(Object value, {toEncodable(var object)}) { … } -``` - -The codec can be instantiated with arguments that are used as default -values, unless they are overridden by the named arguments during the -`encode()`/`decode()` call. - -```dart -const JsonCodec({reviver(var key, var value), toEncodable(var object)}) - ... -``` - -As a general rule: if a codec can be configured, it should add named arguments -to the `encode()`/`decode()` methods and allow their defaults to be -set in constructors. -When possible, codec constructors should be `const` constructors. - -## Converter - -Converters, and in particular their `convert()` methods, are -where the real conversions happen: - -```dart -T convert(S input); // where T is the target and S the source type. -``` - -A minimal converter implementation only needs to extend the -[Converter]({{site.dart-api}}/dart-convert/Converter-class.html) class and -implement the `convert()` method. Similar to the Codec class, converters can be -made configurable by extending the constructors and adding named arguments to -the `convert()` method. - -Such a minimal converter works in synchronous settings, but -does not work when used with chunks (either synchronously or asynchronously). In -particular, such a simple converter doesn't work as a transformer (one of the -nicer features of Converters). A fully implemented converter implements the -[StreamTransformer]({{site.dart-api}}/dart-async/StreamTransformer-class.html) -interface and can thus be given to the `Stream.transform()` method. - -Probably the most common use case is the decoding of UTF-8 with -[utf8.decoder]({{site.dart-api}}/dart-convert/Utf8Codec-class.html): - -```dart -File.openRead().transform(utf8.decoder). -``` +The top-level functions [`jsonEncode`][] and [`jsonDecode`][] +are shorthand for [`json.encode`][] and [`json.decode`][]. +The same pattern applies to some other built-in converters, +such as [`base64Decode`][] being a shorthand for [`base64.decode`][]. -## Chunked conversion - -The concept of chunked conversions can be confusing, but at its core, it is -relatively simple. When a chunked conversion (including a stream -transformation) is started, the converter's -[startChunkedConversion]({{site.dart-api}}/dart-convert/Converter/startChunkedConversion.html) -method is invoked with an output- -sink as argument. The method then returns an input sink into which the caller -puts data. - -![Chunked conversion](/assets/img/articles/converters-and-codecs/chunked-conversion.png) - -**Note**: An asterisk (`*`) in the diagram represents optional multiple calls. - -In the diagram, the first step consists of creating an `outputSink` that should -be filled with the converted data. Then, the user invokes the -`startChunkedConversion()` method of the converter with the output sink. -The result is an input sink with methods `add()` and `close()`. - -At a later point, the code that started the chunked conversion invokes, -possibly multiple times, the `add()` method with -some data. The data is converted by the input sink. If the converted data is -ready the input sink sends it to the output sink, possibly with multiple -`add()` calls. Eventually the user finishes the conversion by invoking -`close()`. At this point any remaining converted data is sent from the input -sink to the output sink and the output sink is closed. - -Depending on the converter the input sink may need to buffer parts of the -incoming data. For example, a line-splitter that receives `ab\ncd` as the first -chunk can safely invoke its output sink with `ab`, but needs to wait for the -next data (or the `close()` call) before it can handle `cd`. If the next data is -`e\nf`, the input sink must concatenate `cd` and `e` and invoke the output sink -with the string `cde`, while buffering `f` for the next data event (or the -`close()` call). - -The complexity of the input sink (in combination with the converter) varies. -Some chunked conversions are trivially mapped to the non-chunked versions (like -a String→String converter that removes the character `a`), while others are -more complicated. A safe, although inefficient (and usually unrecommended) -way to implement the chunked conversion is to buffer and concatenate all the -incoming data and to do the conversion in one go. This is, how the JSON decoder -is currently (January 2014) implemented. - -Interestingly, the type of chunked conversion cannot be extrapolated from its -synchronous conversion. For example, the -[HtmlEscape]({{site.dart-api}}/dart-convert/HtmlEscape-class.html) -converter synchronously -converts Strings to Strings, and asynchronously converts chunks of Strings to -chunks of Strings (String→String). The -[LineSplitter]({{site.dart-api}}/dart-convert/LineSplitter-class.html) -converter synchronously -converts Strings to List (the individual lines). Despite the difference -in the synchronous signature, the chunked version of the LineSplitter converter -has the same signature as -HtmlEscape: String→String. In this case each individual output chunk -represents one line. +You can also use the [`encoder`][] and [`decoder`][] properties +to access the underlying converters: ```dart import 'dart:convert'; -import 'dart:async'; -void main() async { - // HtmlEscape synchronously converts Strings to Strings. - print(const HtmlEscape().convert("foo")); // "foo". - // When used in a chunked way it converts from Strings - // to Strings. - var stream = new Stream.fromIterable(["f", "o", "o"]); - print(await (stream.transform(const HtmlEscape()) - .toList())); // ["f", "o", "o"]. - - // LineSplitter synchronously converts Strings to Lists of String. - print(const LineSplitter().convert("foo\nbar")); // ["foo", "bar"] - // However, asynchronously it converts from Strings to Strings (and - // not Lists of Strings). - var stream2 = new Stream.fromIterable(["fo", "o\nb", "ar"]); - print("${await (stream2.transform(const LineSplitter()) - .toList())}"); -} -``` - -In general, the type of the chunked conversion is determined by the most -useful case when used as a StreamTransformer. - -### ChunkedConversionSink - -[ChunkedConversionSinks]({{site.dart-api}}/dart-convert/ChunkedConversionSink-class.html) -are used to add new data to a -converter or as output from converters. The basic ChunkedConversionSink comes -with two methods: `add()` and `close()`. These have the same functionality as in -all other sinks of the system such as -[StringSinks]({{site.dart-api}}/dart-core/StringSink-class.html) -or -[StreamSinks]({{site.dart-api}}/dart-async/StreamSink-class.html). - -The ChunkedConversionSinks semantics are similar to that of -[IOSinks]({{site.dart-api}}/dart-io/IOSink-class.html): -data added to the -sink must not be modified unless it can be guaranteed that the data has been -handled. For Strings this is not a problem (since they are immutable), but for -lists of bytes it frequently means allocating a fresh copy of the list. This -can be inefficient and the dart:convert library thus comes with subclasses of -ChunkedConversionSink that support more efficient ways of passing data. - -For instance, the -[ByteConversionSink]({{site.dart-api}}/dart-convert/ByteConversionSink-class.html), -has the additional method: +void main() { + final bytes = utf8.encoder.convert('Hello, 世界!'); + print(bytes); // [72, 101, 108, 108, 111, ...] -```dart -void addSlice(List chunk, int start, int end, bool isLast); + final text = utf8.decoder.convert(bytes); + print(text); // Hello, 世界! +} ``` -Semantically, it -accepts a list (which may not be held onto), the sub-range that the converter -operates on, and a boolean `isLast`, which can be set instead of calling -`close()`. +The shorthands and the default top-level codec instances use +a default configuration of the underlying converters. +If you want to customize them, you can +create custom instances of the converters and codecs: ```dart import 'dart:convert'; void main() { - var outSink = new ChunkedConversionSink.withCallback((chunks) { - print(chunks.single); // 𝅘𝅥𝅯 - }); - - var inSink = utf8.decoder.startChunkedConversion(outSink); - var list = [0xF0, 0x9D]; - inSink.addSlice(list, 0, 2, false); - // Since we used `addSlice` we are allowed to reuse the list. - list[0] = 0x85; - list[1] = 0xA1; - inSink.addSlice(list, 0, 2, true); + // Pretty-print JSON with two-space indentation. + const encoder = JsonEncoder.withIndent(' '); + print(encoder.convert({'name': 'Dash', 'age': 5})); + // { + // "name": "Dash", + // "age": 5 + // } + + // Leniently decode UTF-8 that might contain invalid byte sequences + // instead of throwing a FormatException. + const lenientUtf8 = Utf8Codec(allowMalformed: true); + final decoded = lenientUtf8.decode([0x48, 0x65, 0xFF, 0x6C]); + print(decoded); // He�l } ``` -As a user of the chunked conversion sink (which is used both as input and output -of converters), this simply provides more choice. The fact that the list is not -held onto, means that you can use a cache and reuse that one for every call. -Combining `add()` with `close()` may help the receiver in that it can avoid -buffering data. Accepting sub-lists avoids expensive calls to `subList()` -(to copy the data). - -The drawback of this interface is that it is more complicated to implement. To -ease the pain for developers, every improved chunked conversion sink of -dart:convert also comes with a base class that implements all methods except one -(which is abstract). The implementor of the conversion sink can then decide -whether to take advantage of the additional methods. +[`encode`]: {{site.dart-api}}/dart-convert/Codec/encode.html +[`decode`]: {{site.dart-api}}/dart-convert/Codec/decode.html +[`encoder`]: {{site.dart-api}}/dart-convert/Codec/encoder.html +[`decoder`]: {{site.dart-api}}/dart-convert/Codec/decoder.html -**Note**: _Chunked conversion sinks *must* extend the corresponding base class. -This assures that adding functionality to the existing sink interfaces does -not break the extended sinks._ +[`jsonEncode`]: {{site.dart-api}}/dart-convert/jsonEncode.html +[`jsonDecode`]: {{site.dart-api}}/dart-convert/jsonDecode.html +[`json.encode`]: {{site.dart-api}}/dart-convert/JsonCodec/encode.html +[`json.decode`]: {{site.dart-api}}/dart-convert/JsonCodec/decode.html +[`base64Decode`]: {{site.dart-api}}/dart-convert/base64Decode.html +[`base64.decode`]: {{site.dart-api}}/dart-convert/Base64Codec/decode.html -## Example +## Compose codecs with fuse -This section shows all the steps needed to create a simple encryption -converter and how a custom ChunkedConversionSink can improve performance. - -Let's start with the simple synchronous converter, -whose encryption routine simply rotates bytes by the given key: +One of the most powerful features of codecs is composition. +The [`fuse`][codec-fuse] method on `Codec` combines two codecs into one, +potentially eliminating the intermediate representation: ```dart import 'dart:convert'; -/// A simple extension of Rot13 to bytes and a key. -class RotConverter extends Converter, List> { - final _key; - const RotConverter(this._key); - - List convert(List data, { int key }) { - if (key == null) key = this._key; - var result = new List(data.length); - for (int i = 0; i < data.length; i++) { - result[i] = (data[i] + key) % 256; - } - return result; - } -} -``` - -The corresponding Codec class is also simple: - -```dart -class Rot extends Codec, List> { - final _key; - const Rot(this._key); +// Create a codec that goes directly from Dart objects to UTF-8 bytes. +final jsonUtf8 = json.fuse(utf8); - List encode(List data, { int key }) { - if (key == null) key = this._key; - return new RotConverter(key).convert(data); - } - - List decode(List data, { int key }) { - if (key == null) key = this._key; - return new RotConverter(-key).convert(data); - } +void main() { + // Encode directly to bytes without an intermediate string. + final bytes = jsonUtf8.encode({'greeting': 'hello'}); + print(bytes); // [123, 34, 103, ...] - RotConverter get encoder => new RotConverter(_key); - RotConverter get decoder => new RotConverter(-_key); + // Decode directly from bytes without an intermediate string. + final data = jsonUtf8.decode(bytes) as Map; + print(data['greeting']); // hello } ``` -We can (and should) avoid some of the `new` allocations, but for simplicity we -allocate a new instance of RotConverter every time one is needed. +When you fuse `json` with `utf8`, the result isn't just +a wrapper that calls `utf8.encode(json.encode(data))`. +The `JsonCodec` class overrides `fuse` to use [`JsonUtf8Encoder`][], +an optimized encoder implementation that directly +writes UTF-8 bytes without creating an intermediate JSON string. +This makes `json.fuse(utf8)` both more convenient and more efficient +than encoding in two separate steps. -This is how we use the Rot codec: +Converters also have a [`fuse`][converter-fuse] method for +composing one-way transformations: ```dart -const Rot ROT128 = const Rot(128); -const Rot ROT1 = const Rot(1); +import 'dart:convert'; void main() { - print(const RotConverter(128).convert([0, 128, 255, 1])); // [128, 0, 127, 129] - print(const RotConverter(128).convert([128, 0, 127, 129])); // [0, 128, 255, 1] - print(const RotConverter(-128).convert([128, 0, 127, 129]));// [0, 128, 255, 1] - - print(ROT1.decode(ROT1.encode([0, 128, 255, 1]))); // [0, 128, 255, 1] - print(ROT128.decode(ROT128.encode([0, 128, 255, 1]))); // [0, 128, 255, 1] + // Chain two converters: JSON encode, then UTF-8 encode. + final encoder = json.encoder.fuse(utf8.encoder); + final bytes = encoder.convert({'key': 'value'}); + print(bytes); // [123, 34, 107, ...] } ``` -We are on the right track. The codec works, but it is still missing the chunked -encoding part. Because each byte is encoded separately we can fall back to -the synchronous conversion method: - -```dart -class RotConverter { - ... - RotSink startChunkedConversion(sink) { - return new RotSink(_key, sink); - } -} +[codec-fuse]: {{site.dart-api}}/dart-convert/Codec/fuse.html +[converter-fuse]: {{site.dart-api}}/dart-convert/Converter/fuse.html +[`JsonUtf8Encoder`]: {{site.dart-api}}/dart-convert/JsonUtf8Encoder-class.html -class RotSink extends ChunkedConversionSink> { - final _converter; - final ChunkedConversionSink> _outSink; - RotSink(key, this._outSink) : _converter = new RotConverter(key); +## Transform streams with converters - void add(List data) { - _outSink.add(_converter.convert(data)); - } +Because every `Converter` implements [`StreamTransformer`][], +you can use converters to process data as it flows through a stream. +This can be important when working with large files or network data +that shouldn't be loaded entirely into memory at once. - void close() { - _outSink.close(); - } -} -``` - -Now, we can use the converter with chunked conversions or even for stream -transformations: +Use the [`transform`][] method to apply a converter to a stream: ```dart +import 'dart:convert'; import 'dart:io'; -void main(List args) { - String inFile = args[0]; - String outFile = args[1]; - int key = int.parse(args[2]); - new File(inFile) +void main() async { + // Read a file as a stream of bytes, decode UTF-8 into a string, + // then split the single string into lines. + final lines = File('data.txt') .openRead() - .transform(new RotConverter(key)) - .pipe(new File(outFile).openWrite()); -} -``` - -### Specialized ChunkedConversionSinks - -For many purposes, the current version of Rot is sufficient. That is, the -benefit of improvements would be outweighed by the cost of more complex code -and test requirements. Let's assume, however, -that the performance of the converter is critical -(it's on the hot path and up on the profile). -We furthermore assume that -the cost of allocating a new list for every chunk is killing performance -(a reasonable assumption). - -We start by making the allocation cost cheaper: by using a -[typed byte-list]({{site.dart-api}}/dart-typed_data/Uint8List-class.html) -we can reduce the size of the allocated list by a factor of 8 (on 64-bit -machines). This one line change doesn't remove the allocation, but makes it much -cheaper. - -We can also avoid the allocation altogether if we overwrite the input. In -the following version of RotSink, we add a new method `addModifiable()` that -does exactly that: - -```dart -class RotSink extends ChunkedConversionSink> { - final _key; - final ChunkedConversionSink> _outSink; - RotSink(this._key, this._outSink); - - void add(List data) { - addModifiable(new Uint8List.fromList(data)); - } - - void addModifiable(List data) { - for (int i = 0; i < data.length; i++) { - data[i] = (data[i] + _key) % 256; - } - _outSink.add(data); - } + .transform(utf8.decoder) + .transform(const LineSplitter()); - void close() { - _outSink.close(); + await for (final line in lines) { + print(line); } } ``` -For simplicity we propose a new method that consumes a complete list. A more -advanced method (for example `addModifiableSlice()`) would take range arguments -(`from`, `to`) and an `isLast` boolean as arguments. - -This new method is not yet used by transformers, but we can already use it when -invoking `startChunkedConversion()` explicitly. +You can build longer transformating pipelines by +chaining multiple `transform` calls or by fusing converters first: ```dart -void main() { - var outSink = new ChunkedConversionSink.withCallback((chunks) { - print(chunks); // [[31, 32, 33], [24, 25, 26]] - }); - var inSink = new RotConverter(30).startChunkedConversion(outSink); - inSink.addModifiable([1, 2, 3]); - inSink.addModifiable([250, 251, 252]); - inSink.close(); -} -``` +import 'dart:convert'; +import 'dart:io'; -In this small example, performance isn't visibly different, -but internally the -chunked conversion avoids allocating new lists for the individual chunks. -For two small chunks, it doesn't make a difference, but -if we implement this for the stream transformer, -encrypting a bigger file can be noticeably faster. - -To do this, -we can make use of the undocumented feature that IOStreams provide modifiable lists. -We could now simply rewrite `add()` and -point it directly to `addModifiable()`. In general, this is not safe, -and -such a converter would be the potential source of hard-to-track bugs. Instead, -we write a converter that does the unmodifiable-to-modifiable conversion -explicitly, and then fuse the two converters. +void main() async { + // Read a JSON file and decode it in a single streaming pipeline. + final stream = File('data.json') + .openRead() + .transform(utf8.decoder) + .transform(json.decoder); -```dart -class ToModifiableConverter extends Converter, List> { - List convert(List data) => data; - ToModifiableSink startChunkedConversion(RotSink sink) { - return new ToModifiableSink(sink); + await for (final value in stream) { + print(value); } } - -class ToModifiableSink - extends ChunkedConversionSink, List> { - final RotSink sink; - ToModifiableSink(this.sink); - - void add(List data) { sink.addModifiable(data); } - void close() { sink.close(); } -} ``` -ToModifiableSink just signals the next sink that the incoming chunk -is modifiable. We can use this to make our pipeline more efficient: +Streaming conversion is especially useful with compression. +The following example decompresses and decodes a gzipped text file +without loading the entire file into memory: ```dart -void main(List args) { - String inFile = args[0]; - String outFile = args[1]; - int key = int.parse(args[2]); - new File(inFile) - .openRead() - .transform( - new ToModifiableConverter().fuse(new RotConverter(key))) - .pipe(new File(outFile).openWrite()); -} -``` - -On my machine, this small modification brought the encryption time of an 11MB -file from 450ms down to 260ms. We achieved this speed up without losing -compatibility with existing codecs (with regard to the `fuse()` method) -and the converter still functions as a stream transformer. +import 'dart:convert'; +import 'dart:io'; -Reusing the input works nicely with other -converters and not just with our Rot cipher. We should therefore make an -interface that generalizes the concept. For simplicity, we named it -`CipherSink`, although it has, of course, uses outside the encryption world. +void main() async { + final lines = File('log.gz') + .openRead() + .transform(gzip.decoder) + .transform(utf8.decoder) + .transform(const LineSplitter()); -```dart -abstract class CipherSink - extends ChunkedConversionSink, List> { - void addModifiable(List data) { add(data); } + await for (final line in lines) { + print(line); + } } ``` -We can then make our RotSink private and expose the CipherSink instead. -Other developers can now reuse our work (CipherSink and ToModifiableConverter) -and benefit from it. +[`StreamTransformer`]: {{site.dart-api}}/dart-async/StreamTransformer-class.html +[`transform`]: /dart-async/Stream/transform.html -But we are not done yet. +## Character encodings -Although we won't make the cipher faster anymore, -we can improve the output side of our Rot converter. -Take, for instance, the fusion of two encryptions: +The `Encoding` base class is specialized for character-encoding codecs, +where its subclasses encode and decode strings to lists of bytes. +It adds a `name` property and a static `getByName` method +for looking up built-in, supported encodings by their [IANA name][]: ```dart -void main(List args) { - String inFile = args[0]; - String outFile = args[1]; - int key = int.parse(args[2]); - // Double-strength cipher running the Rot-cipher twice. - var transformer = new ToModifiableConverter() - .fuse(new RotConverter(key)) // <= fused RotConverters. - .fuse(new RotConverter(key)); - new File(inFile) - .openRead() - .transform(transformer) - .pipe(new File(outFile).openWrite()); +import 'dart:convert'; + +void main() { + final encoding = Encoding.getByName('utf-8'); + print(encoding?.name); // utf-8 } ``` -Since the first RotConverter invokes `outSink.add()`, the second RotConverter -assumes that input cannot be modified and allocates a copy. We can work around -this by sandwiching a ToModifiableConverter in between the two ciphers: +The built-in encodings include: -```dart - var transformer = new ToModifiableConverter() - .fuse(new RotConverter(key)) - .fuse(new ToModifiableConverter()) - .fuse(new RotConverter(key)); -``` - -This works, but is hackish. We want the RotConverters to work without -intermediate converters. The first cipher should look at the outSink and -determines if it is a CipherSink or not. We can do this either, -whenever we want to add a new chunk, -or at the beginning when we start a chunked -conversion. We prefer the latter approach: +- UTF-8 with [`Utf8Codec`][] and a `name` of "utf-8". +- ASCII with [`AsciiCodec`][] and a `name` of "us-ascii". +- ISO Latin-1 with [`Latin1Codec`][] and a `name` of "iso-8859-1". -```dart - /// Works more efficiently if given a CipherSink as argument. - CipherSink startChunkedConversion( - ChunkedConversionSink> sink) { - if (sink is! CipherSink) sink = new _CipherSinkAdapter(sink); - return new _RotSink(_key, sink); - } -``` +For other character sets such as UTF-16, GBK, or Shift-JIS, +check out third-party packages like [`package:charset`][] that +provide many codecs implementing the `Encoding` interface. -_CipherSinkAdapter is simply: +`Encoding` also provides a default implementation of [`decodeStream`][]. +In contrast to `Stream.transform` which returns a `Stream`, +the `decodeStream` method reads a byte stream and +returns a single decoded `String` (wrapped in a `Future`). ```dart -class _CipherSinkAdapter implements CipherSink { - ChunkedConversionSink, List> sink; - _CipherSinkAdapter(this.sink); +import 'dart:convert'; +import 'dart:io'; - void add(data) { sink.add(data); } - void addModifiable(data) { sink.add(data); } - void close() { sink.close(); } +void main() async { + // Read and decode an entire file as a single string. + final stream = File('data.txt').openRead(); + final content = await utf8.decodeStream(stream); + print(content); } ``` -We now only need to change the _RotSink to take advantage of the fact that it -always receives a CipherSink as an argument to its constructor: +Many `dart:io` APIs accept an `Encoding` parameter, +making it straightforward to read and write files with +different character encodings: ```dart -class _RotSink extends CipherSink { - final _key; - final CipherSink _outSink; // <= always a CipherSink. - _RotSink(this._key, this._outSink); - - void add(List data) { - addModifiable(data.toList()); - } - - void addModifiable(List data) { - for (int i = 0; i < data.length; i++) { - data[i] = (data[i] + _key) % 256; - } - _outSink.addModifiable(data); // <= safe to call addModifiable. - } +import 'dart:convert'; +import 'dart:io'; - void close() { - _outSink.close(); - } +void main() async { + final file = File('output.txt'); + // Write a file in Latin-1 encoding. + await file.writeAsString('café', encoding: latin1); } ``` -With these changes our super secure, double cipher won't allocate any new lists -and our work is done. - -Thanks to Lasse Reichstein Holst Nielsen, Anders Johnsen, and Matias Meno who -were a great help in writing this article. +[`decodeStream`]: {{site.dart-api}}/dart-convert/Encoding/decodeStream.html +[`package:charset`]: {{site.pub-pkg}}/charset +[iana name]: https://www.iana.org/assignments/character-sets/character-sets.xhtml + +## Codecs versus `fromJson` and `toJson` {:#when-to-use-codecs} + +For serialization of application-level data and objects, +where you want to convert between Dart model classes and JSON, +the `fromJson`/`toJson` convention supported by packages such as +[`package:json_serializable`][] is the idiomatic approach in Dart apps. +These tools generate simple factory constructors and methods +that work directly with `Map`. + +The `Codec` and `Converter` classes serve a different purpose. +They're designed for data format transformations: +encoding schemes, compression, character sets, and +other conversions where streaming support, bidirectional symmetry, +and composability through `fuse()` matter. + +If the conversion is about how data is _represented_, +such as with bytes, strings, or compressed data, +the codec pattern is a natural fit. +If the conversion is about how data is _structured_, +such as mapping JSON fields to Dart class properties, +use `fromJson`/`toJson`. + +If you do want to build your own codecs and converters, +check out [Build custom codecs and converters][]. + +[`package:json_serializable`]: {{site.pub-pkg}}/json_serializable +[`package:freezed`]: {{site.pub-pkg}}/freezed +[Build custom codecs and converters]: /libraries/convert/build-custom-codecs diff --git a/src/content/libraries/convert/index.md b/src/content/libraries/convert/index.md new file mode 100644 index 0000000000..fd8afae1c5 --- /dev/null +++ b/src/content/libraries/convert/index.md @@ -0,0 +1,12 @@ +--- +title: Dart's support for data conversion +shortTitle: dart:convert library +breadcrumb: Convert +description: >- + Learn about Dart's library support for + converting between and transforming different data representations +# This page exists for configuring the breadcrumb entries of descendants. +# It is redirected to a different page by the Firebase config. +sitemap: false +noindex: true +--- diff --git a/src/data/sidenav/default.yml b/src/data/sidenav/default.yml index 3a7a169618..5d7182ffb6 100644 --- a/src/data/sidenav/default.yml +++ b/src/data/sidenav/default.yml @@ -128,6 +128,13 @@ permalink: /libraries/async/using-streams - title: Creating streams permalink: /libraries/async/creating-streams + - title: Converters and codecs + permalink: /libraries/dart-convert + children: + - title: Use converters and codecs + permalink: /libraries/convert/converters-and-codecs + - title: Build custom codecs and converters + permalink: /libraries/convert/build-custom-codecs - title: Effective Dart expanded: false From 81cf88f1aa1070877d656e86a05cbdbcaa055d0c Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 11 Mar 2026 00:09:46 +0800 Subject: [PATCH 02/11] Add missing link reference definitions --- src/content/libraries/convert/build-custom-codecs.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md index 5cf750bcae..f370a2f19d 100644 --- a/src/content/libraries/convert/build-custom-codecs.md +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -626,6 +626,10 @@ This helps your sink continue to work correctly if new members are added to super types in the future. ::: +[`StringConversionSink`]: {{site.dart-api}}/dart-convert/StringConversionSink-class.html +[`ByteConversionSink`]: {{site.dart-api}}/dart-convert/ByteConversionSink-class.html +[`ChunkedConversionSink`]: {{site.dart-api}}/dart-convert/ChunkedConversionSink-class.html + ### Respect data ownership in sinks Data passed to a sink's `add` method shouldn't be @@ -678,5 +682,5 @@ This makes the codec easy to discover and use. an introduction to using the built-in codecs and converters. [library-api-docs]: {{site.dart-api}}/dart-convert -[pkg-convert]: {{site.pub-pkg}}/convert +[`package:convert`]: {{site.pub-pkg}}/convert [Converters and codecs]: /libraries/convert/converters-and-codecs From 3620dde5dde3382019ad424d0312a8991e26dd48 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 11 Mar 2026 00:17:38 +0800 Subject: [PATCH 03/11] Fix broken link reference --- src/content/libraries/convert/converters-and-codecs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index 13588de9de..3089f25b89 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -290,7 +290,7 @@ void main() async { ``` [`StreamTransformer`]: {{site.dart-api}}/dart-async/StreamTransformer-class.html -[`transform`]: /dart-async/Stream/transform.html +[`transform`]: {{site.dart-api}}/dart-async/Stream/transform.html ## Character encodings From c6764a60f334c80dc968129f02d382e38045d56f Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 11 Mar 2026 00:26:40 +0800 Subject: [PATCH 04/11] Improve styling and consistency of built-in tables --- .../convert/converters-and-codecs.md | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index 3089f25b89..7a94284b63 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -55,13 +55,15 @@ codecs for several common formats. It also provides top-level variables with default-configured codec instances for convenience: -| Codec class | Encodes from | Encodes to | Default instance | -|-------------------|--------------|-----------------|------------------| -| [`JsonCodec`][] | Dart objects | JSON string | [`json`][] | -| [`Utf8Codec`][] | `String` | UTF-8 bytes | [`utf8`][] | -| [`AsciiCodec`][] | `String` | ASCII bytes | [`ascii`][] | -| [`Latin1Codec`][] | `String` | Latin-1 bytes | [`latin1`][] | -| [`Base64Codec`][] | Bytes | Base64 `String` | [`base64`][] | +| Codec class | Encodes from | Encodes to | Default instance | +|-------------------|--------------|---------------|------------------| +| [`JsonCodec`][] | Objects | JSON string | [`json`][] | +| [`Utf8Codec`][] | String | UTF-8 bytes | [`utf8`][] | +| [`AsciiCodec`][] | String | ASCII bytes | [`ascii`][] | +| [`Latin1Codec`][] | String | Latin-1 bytes | [`latin1`][] | +| [`Base64Codec`][] | Bytes | Base64 string | [`base64`][] | + +{:.table .table-striped} The `dart:io` library additionally provides a few compression codecs: @@ -70,26 +72,28 @@ The `dart:io` library additionally provides a few compression codecs: | [`GZipCodec`][] | Bytes | GZip-compressed bytes | [`gzip`][] | | [`ZLibCodec`][] | Bytes | ZLib-compressed bytes | [`zlib`][] | +{:.table .table-striped} + Besides the provided codecs and their converters, `dart:convert` also provides the [`HtmlEscape`][] converter, a converter that escapes HTML special characters, and [`LineSplitter`][], a stream transformer that splits strings into individual lines. [`JsonCodec`]: {{site.dart-api}}/dart-convert/JsonCodec-class.html -[`json`]: {{site.dart-api}}/dart-convert/json.html +[`json`]: {{site.dart-api}}/dart-convert/json-constant.html [`Utf8Codec`]: {{site.dart-api}}/dart-convert/Utf8Codec-class.html -[`utf8`]: {{site.dart-api}}/dart-convert/utf8.html +[`utf8`]: {{site.dart-api}}/dart-convert/utf8-constant.html [`AsciiCodec`]: {{site.dart-api}}/dart-convert/AsciiCodec-class.html -[`ascii`]: {{site.dart-api}}/dart-convert/ascii.html +[`ascii`]: {{site.dart-api}}/dart-convert/ascii-constant.html [`Latin1Codec`]: {{site.dart-api}}/dart-convert/Latin1Codec-class.html -[`latin1`]: {{site.dart-api}}/dart-convert/latin1.html +[`latin1`]: {{site.dart-api}}/dart-convert/latin1-constant.html [`Base64Codec`]: {{site.dart-api}}/dart-convert/Base64Codec-class.html -[`base64`]: {{site.dart-api}}/dart-convert/base64.html +[`base64`]: {{site.dart-api}}/dart-convert/base64-constant.html [`GZipCodec`]: {{site.dart-api}}/dart-io/GZipCodec-class.html -[`gzip`]: {{site.dart-api}}/dart-io/gzip.html +[`gzip`]: {{site.dart-api}}/dart-io/gzip-constant.html [`ZLibCodec`]: {{site.dart-api}}/dart-io/ZLibCodec-class.html -[`zlib`]: {{site.dart-api}}/dart-io/zlib.html +[`zlib`]: {{site.dart-api}}/dart-io/zlib-constant.html [`HtmlEscape`]: {{site.dart-api}}/dart-convert/HtmlEscape-class.html [`LineSplitter`]: {{site.dart-api}}/dart-convert/LineSplitter-class.html From ee421f0755271f926ce5c955d6295c1cce14818f Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 22:47:14 +0800 Subject: [PATCH 05/11] Add code excerpts --- .../lib/convert/build_custom_codecs.dart | 164 ++++++++++++++++++ .../convert/build_custom_codecs_complete.dart | 126 ++++++++++++++ .../convert/build_custom_codecs_stream.dart | 43 +++++ .../lib/convert/converters_and_codecs.dart | 76 ++++++++ .../lib/convert/converters_and_codecs_io.dart | 66 +++++++ .../build_custom_codecs_test_example.dart | 65 +++++++ .../libraries/convert/build-custom-codecs.md | 100 +++++++---- .../convert/converters-and-codecs.md | 36 ++-- 8 files changed, 632 insertions(+), 44 deletions(-) create mode 100644 examples/libraries/lib/convert/build_custom_codecs.dart create mode 100644 examples/libraries/lib/convert/build_custom_codecs_complete.dart create mode 100644 examples/libraries/lib/convert/build_custom_codecs_stream.dart create mode 100644 examples/libraries/lib/convert/converters_and_codecs.dart create mode 100644 examples/libraries/lib/convert/converters_and_codecs_io.dart create mode 100644 examples/libraries/test/convert/build_custom_codecs_test_example.dart diff --git a/examples/libraries/lib/convert/build_custom_codecs.dart b/examples/libraries/lib/convert/build_custom_codecs.dart new file mode 100644 index 0000000000..b7949341ff --- /dev/null +++ b/examples/libraries/lib/convert/build_custom_codecs.dart @@ -0,0 +1,164 @@ +import 'dart:convert'; + +// #docregion caesar-encoder-sink +/// A [StringConversionSink] that applies a Caesar cipher [_shift] and +/// forwards the result to the [_output] sink. +class _CaesarEncoderSink extends StringConversionSinkBase { + final int _shift; + final StringConversionSink _output; + + _CaesarEncoderSink(this._shift, this._output); + + @override + void addSlice(String chunk, int start, int end, bool isLast) { + final buffer = StringBuffer(); + for (var i = start; i < end; i++) { + buffer.writeCharCode(_shiftCodeUnit(chunk.codeUnitAt(i), _shift)); + } + _output.add(buffer.toString()); + if (isLast) { + _output.close(); + } + } + + @override + void close() => _output.close(); +} +// #enddocregion caesar-encoder-sink + +// #docregion basic-encoder, caesar-encoder +/// Encodes a string by shifting each letter forward in the alphabet. +class CaesarEncoder extends Converter { + /// The number of positions to shift each letter forward. + final int shift; + + /// Creates an encoder that shifts each letter by [shift] positions. + const CaesarEncoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, shift)); + } + return buffer.toString(); + } + // #enddocregion basic-encoder + + // #docregion start-chunked-conversion + @override + StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. + // ignore: close_sinks + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(shift, stringSink); + } + + // #enddocregion start-chunked-conversion + // #docregion basic-encoder +} +// #enddocregion caesar-encoder + +// #docregion shift-code-unit +/// Shifts the specified [codeUnit] by +/// [shift] positions in the alphabet. +/// +/// Only shifts lowercase ASCII letters (a-z). +/// All other characters are returned unchanged. +int _shiftCodeUnit(int codeUnit, int shift) { + const a = 0x61; + const z = 0x7A; + if (codeUnit >= a && codeUnit <= z) { + return a + (codeUnit - a + shift) % 26; + } + return codeUnit; +} +// #enddocregion basic-encoder, shift-code-unit + +// #docregion basic-decoder +/// Decodes a Caesar-cipher-encoded string by +/// shifting each letter backward in the alphabet. +class CaesarDecoder extends Converter { + /// The number of positions to shift each letter backward. + final int shift; + + /// Creates a decoder that reverses a Caesar cipher with [shift] positions. + const CaesarDecoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + // Shift backward by shifting forward by (26 - shift). + buffer.writeCharCode(_shiftCodeUnit(codeUnit, 26 - shift)); + } + return buffer.toString(); + } + // #enddocregion basic-decoder + + @override + StringConversionSink startChunkedConversion(Sink sink) { + // ignore: close_sinks + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(26 - shift, stringSink); + } + + // #docregion basic-decoder +} +// #enddocregion basic-decoder + +// #docregion caesar-codec +/// A codec that encodes and decodes strings using a +/// [Caesar cipher](https://wikipedia.org/wiki/Caesar_cipher). +class CaesarCodec extends Codec { + /// The number of positions to shift each letter. + final int shift; + + @override + CaesarEncoder get encoder => CaesarEncoder(shift); + + @override + CaesarDecoder get decoder => CaesarDecoder(shift); + + /// Creates a Caesar cipher codec with the given [shift]. + const CaesarCodec(this.shift); + + /// Creates a [CaesarCodec] that uses ROT13 encoding. + const CaesarCodec.rot13() : shift = 13; +} +// #enddocregion caesar-codec + +// #docregion use-encoder +void useEncoderExample() { + const encoder = CaesarEncoder(3); + print(encoder.convert('hello')); // khoor + print(encoder.convert('xyz')); // abc +} +// #enddocregion use-encoder + +// #docregion use-codec +void useCodecExample() { + const caesar = CaesarCodec(3); + + final encoded = caesar.encode('hello'); + print(encoded); // khoor + + final decoded = caesar.decode(encoded); + print(decoded); // hello + + // The `inverted` getter returns a new codec that + // applies converts in the inverse direction of the codec. + final inverted = caesar.inverted; + print(inverted.encode('khoor')); // hello +} +// #enddocregion use-codec + +// #docregion top-level-instance +/// A [CaesarCodec] with the standard shift of 13 (ROT13). +const rot13 = CaesarCodec.rot13(); +// #enddocregion top-level-instance diff --git a/examples/libraries/lib/convert/build_custom_codecs_complete.dart b/examples/libraries/lib/convert/build_custom_codecs_complete.dart new file mode 100644 index 0000000000..497cf7d74d --- /dev/null +++ b/examples/libraries/lib/convert/build_custom_codecs_complete.dart @@ -0,0 +1,126 @@ +// #docregion complete +import 'dart:convert'; + +/// Shifts the specified [codeUnit] by +/// [shift] positions in the alphabet. +/// +/// Only shifts lowercase ASCII letters (a-z). +/// All other characters are returned unchanged. +int _shiftCodeUnit(int codeUnit, int shift) { + const a = 0x61; + const z = 0x7A; + if (codeUnit >= a && codeUnit <= z) { + return a + (codeUnit - a + shift) % 26; + } + return codeUnit; +} + +/// A [StringConversionSink] that applies a Caesar cipher shift +/// and forwards the result to [_output]. +class _CaesarEncoderSink extends StringConversionSinkBase { + final int _shift; + final StringConversionSink _output; + + _CaesarEncoderSink(this._shift, this._output); + + @override + void addSlice(String chunk, int start, int end, bool isLast) { + final buffer = StringBuffer(); + for (var i = start; i < end; i++) { + buffer.writeCharCode(_shiftCodeUnit(chunk.codeUnitAt(i), _shift)); + } + _output.add(buffer.toString()); + if (isLast) { + _output.close(); + } + } + + @override + void close() => _output.close(); +} + +/// Encodes a string by shifting each letter forward in the alphabet. +class CaesarEncoder extends Converter { + /// The number of positions to shift each letter forward. + final int shift; + + /// Creates an encoder that shifts each letter by [shift] positions. + const CaesarEncoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, shift)); + } + return buffer.toString(); + } + + @override + StringConversionSink startChunkedConversion(Sink sink) { + // ignore: close_sinks + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(shift, stringSink); + } +} + +/// Decodes a Caesar-cipher-encoded string +/// by shifting each letter backward in the alphabet. +class CaesarDecoder extends Converter { + /// The number of positions to shift each letter backward. + final int shift; + + /// Creates a decoder that reverses a Caesar cipher with [shift] positions. + const CaesarDecoder(this.shift); + + @override + String convert(String input) { + final buffer = StringBuffer(); + for (final codeUnit in input.codeUnits) { + buffer.writeCharCode(_shiftCodeUnit(codeUnit, 26 - shift)); + } + return buffer.toString(); + } + + @override + StringConversionSink startChunkedConversion(Sink sink) { + // ignore: close_sinks + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(26 - shift, stringSink); + } +} + +/// A codec that encodes and decodes strings using a +/// [Caesar cipher](https://wikipedia.org/wiki/Caesar_cipher). +class CaesarCodec extends Codec { + /// The number of positions to shift each letter. + final int shift; + + @override + CaesarEncoder get encoder => CaesarEncoder(shift); + + @override + CaesarDecoder get decoder => CaesarDecoder(shift); + + /// Creates a Caesar cipher codec with the given [shift]. + const CaesarCodec(this.shift); + + /// Creates a [CaesarCodec] that uses ROT13 encoding. + const CaesarCodec.rot13() : shift = 13; +} + +void main() { + const codec = CaesarCodec(3); + + final encoded = codec.encode('the quick brown fox'); + print(encoded); // wkh txlfn eurzq ira + + final decoded = codec.decode(encoded); + print(decoded); // the quick brown fox +} + +// #enddocregion complete diff --git a/examples/libraries/lib/convert/build_custom_codecs_stream.dart b/examples/libraries/lib/convert/build_custom_codecs_stream.dart new file mode 100644 index 0000000000..703511db2e --- /dev/null +++ b/examples/libraries/lib/convert/build_custom_codecs_stream.dart @@ -0,0 +1,43 @@ +// #docregion stream-transform, fuse-pipeline +import 'dart:convert'; +import 'dart:io'; + +// #enddocregion stream-transform, fuse-pipeline + +import 'build_custom_codecs.dart'; + +// #docregion stream-transform +void streamTransformExample() async { + const caesar = CaesarCodec(3); + + // Encrypt a file as a stream. + final encrypted = File('message.txt') + .openRead() // + .transform(utf8.decoder) + .transform(caesar.encoder); + + await for (final chunk in encrypted) { + stdout.write(chunk); + } +} +// #enddocregion stream-transform + +// #docregion fuse-pipeline +void fusePipelineExample() async { + const caesar = CaesarCodec(3); + + // Create a codec that encrypts, then compresses. + final encryptAndCompress = caesar.fuse(utf8).fuse(gzip); + + // Write encrypted, compressed data. + final output = File('message.gz').openWrite(); + output.add(encryptAndCompress.encode('Secret message.')); + await output.close(); + + // Read and decrypt. + final bytes = await File('message.gz').readAsBytes(); + final decrypted = encryptAndCompress.decode(bytes); + print(decrypted); // Secret message. +} + +// #enddocregion fuse-pipeline diff --git a/examples/libraries/lib/convert/converters_and_codecs.dart b/examples/libraries/lib/convert/converters_and_codecs.dart new file mode 100644 index 0000000000..4028879442 --- /dev/null +++ b/examples/libraries/lib/convert/converters_and_codecs.dart @@ -0,0 +1,76 @@ +// #docregion encode-decode, encoder-decoder, custom-config, fuse-codec, fuse-converter, encoding-lookup +import 'dart:convert'; + +// #enddocregion encode-decode, encoder-decoder, custom-config, fuse-codec, fuse-converter, encoding-lookup + +// #docregion encode-decode +void encodeDecodeExample() { + // Encode a Dart object to a JSON string. + final jsonString = json.encode({'name': 'Dash', 'age': 5}); + print(jsonString); // {"name":"Dash","age":5} + + // Decode a JSON string back to a Dart object. + final data = json.decode(jsonString) as Map; + print(data['name']); // Dash +} +// #enddocregion encode-decode + +// #docregion encoder-decoder +void encoderDecoderExample() { + final bytes = utf8.encoder.convert('Hello, 世界!'); + print(bytes); // [72, 101, 108, 108, 111, ...] + + final text = utf8.decoder.convert(bytes); + print(text); // Hello, 世界! +} +// #enddocregion encoder-decoder + +// #docregion custom-config +void customConfigExample() { + // Pretty-print JSON with two-space indentation. + const encoder = JsonEncoder.withIndent(' '); + print(encoder.convert({'name': 'Dash', 'age': 5})); + // { + // "name": "Dash", + // "age": 5 + // } + + // Leniently decode UTF-8 that might contain invalid byte sequences + // instead of throwing a FormatException. + const lenientUtf8 = Utf8Codec(allowMalformed: true); + final decoded = lenientUtf8.decode([0x48, 0x65, 0xFF, 0x6C]); + print(decoded); // He�l +} +// #enddocregion custom-config + +// #docregion fuse-codec +// Create a codec that goes directly from Dart objects to UTF-8 bytes. +final Codec> jsonUtf8 = json.fuse(utf8); + +void fuseCodecExample() { + // Encode directly to bytes without an intermediate string. + final bytes = jsonUtf8.encode({'greeting': 'hello'}); + print(bytes); // [123, 34, 103, ...] + + // Decode directly from bytes without an intermediate string. + final data = jsonUtf8.decode(bytes) as Map; + print(data['greeting']); // hello +} +// #enddocregion fuse-codec + +// #docregion fuse-converter +void fuseConverterExample() { + // Chain two converters: JSON encode, then UTF-8 encode. + final encoder = json.encoder.fuse(utf8.encoder); + final bytes = encoder.convert({'key': 'value'}); + print(bytes); // [123, 34, 107, ...] +} +// #enddocregion fuse-converter + +// #docregion encoding-lookup +void encodingLookupExample() { + final encoding = Encoding.getByName('utf-8'); + print(encoding?.name); // utf-8 +} + +// #enddocregion encoding-lookup diff --git a/examples/libraries/lib/convert/converters_and_codecs_io.dart b/examples/libraries/lib/convert/converters_and_codecs_io.dart new file mode 100644 index 0000000000..e591bde40a --- /dev/null +++ b/examples/libraries/lib/convert/converters_and_codecs_io.dart @@ -0,0 +1,66 @@ +// #docregion stream-lines, stream-json, stream-gzip, decode-stream, encoding-param +import 'dart:convert'; +import 'dart:io'; + +// #enddocregion stream-lines, stream-json, stream-gzip, decode-stream, encoding-param + +// #docregion stream-lines +void streamLinesExample() async { + // Read a file as a stream of bytes, decode UTF-8 into a string, + // then split the single string into lines. + final lines = File('data.txt') + .openRead() // + .transform(utf8.decoder) + .transform(const LineSplitter()); + + await for (final line in lines) { + print(line); + } +} +// #enddocregion stream-lines + +// #docregion stream-json +void streamJsonExample() async { + // Read a JSON file and decode it in a single streaming pipeline. + final stream = File('data.json') + .openRead() // + .transform(utf8.decoder) + .transform(json.decoder); + + await for (final value in stream) { + print(value); + } +} +// #enddocregion stream-json + +// #docregion stream-gzip +void streamGzipExample() async { + final lines = File('log.gz') + .openRead() + .transform(gzip.decoder) + .transform(utf8.decoder) + .transform(const LineSplitter()); + + await for (final line in lines) { + print(line); + } +} +// #enddocregion stream-gzip + +// #docregion decode-stream +void decodeStreamExample() async { + // Read and decode an entire file as a single string. + final stream = File('data.txt').openRead(); + final content = await utf8.decodeStream(stream); + print(content); +} +// #enddocregion decode-stream + +// #docregion encoding-param +void encodingParamExample() async { + final file = File('output.txt'); + // Write a file in Latin-1 encoding. + await file.writeAsString('café', encoding: latin1); +} + +// #enddocregion encoding-param diff --git a/examples/libraries/test/convert/build_custom_codecs_test_example.dart b/examples/libraries/test/convert/build_custom_codecs_test_example.dart new file mode 100644 index 0000000000..f67a3c8480 --- /dev/null +++ b/examples/libraries/test/convert/build_custom_codecs_test_example.dart @@ -0,0 +1,65 @@ +// #docregion test-codec +import 'dart:async'; +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:libraries/convert/build_custom_codecs.dart'; + +void main() { + const codec = CaesarCodec(3); + + group('CaesarCodec', () { + test('encode shifts letters forward', () { + expect(codec.encode('abc'), equals('def')); + expect(codec.encode('xyz'), equals('abc')); + }); + + test('decode reverses encode', () { + const original = 'the quick brown fox'; + final encoded = codec.encode(original); + expect(codec.decode(encoded), equals(original)); + }); + + test('non-letter characters pass through unchanged', () { + expect(codec.encode('hello, world!'), equals('khoor, zruog!')); + }); + + test('empty string encodes to empty string', () { + expect(codec.encode(''), equals('')); + }); + + test('inverted codec swaps encode and decode', () { + final inverted = codec.inverted; + expect(inverted.encode('def'), equals('abc')); + expect(inverted.decode('abc'), equals('def')); + }); + + test('chunked conversion matches single-pass conversion', () { + final chunks = []; + final outputSink = ChunkedConversionSink.withCallback( + (accumulated) => chunks.addAll(accumulated), + ); + + // Manually drive a chunked conversion. + final inputSink = codec.encoder.startChunkedConversion(outputSink); + inputSink.add('hel'); + inputSink.add('lo '); + inputSink.add('world'); + inputSink.close(); + + expect(chunks.join(), equals(codec.encode('hello world'))); + }); + + test('chunked conversion works with streams', () async { + const input = 'hello world'; + + // Split the input into chunks and convert as a stream. + final stream = Stream.fromIterable(['hel', 'lo ', 'world']); + final result = await stream.transform(codec.encoder).join(); + + expect(result, equals(codec.encode(input))); + }); + }); +} + +// #enddocregion test-codec diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md index f370a2f19d..0ff618e7de 100644 --- a/src/content/libraries/convert/build-custom-codecs.md +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -5,6 +5,8 @@ description: >- with support for streaming and composition. --- + + The [`dart:convert`][] library defines [`Codec`][] and [`Converter`][] as extensible base classes. By implementing these classes, you give your conversion logic a standard interface @@ -59,13 +61,14 @@ The following [Caesar cipher][] encoder shifts each lowercase letter forward in the alphabet by a given amount. Non-letter characters pass through unchanged: + ```dart -import 'dart:convert'; - /// Encodes a string by shifting each letter forward in the alphabet. class CaesarEncoder extends Converter { + /// The number of positions to shift each letter forward. final int shift; + /// Creates an encoder that shifts each letter by [shift] positions. const CaesarEncoder(this.shift); @override @@ -78,7 +81,8 @@ class CaesarEncoder extends Converter { } } -/// Shifts the specified [codeUnit] by [shift] positions in the alphabet. +/// Shifts the specified [codeUnit] by +/// [shift] positions in the alphabet. /// /// Only shifts lowercase ASCII letters (a-z). /// All other characters are returned unchanged. @@ -94,11 +98,12 @@ int _shiftCodeUnit(int codeUnit, int shift) { You can now use the converter to encode strings: + ```dart void main() { const encoder = CaesarEncoder(3); print(encoder.convert('hello')); // khoor - print(encoder.convert('xyz')); // abc + print(encoder.convert('xyz')); // abc } ``` @@ -112,12 +117,15 @@ You could reuse `CaesarEncoder` directly, but creating a separate class keeps the intent clear and lets you customize behavior if needed: + ```dart /// Decodes a Caesar-cipher-encoded string by /// shifting each letter backward in the alphabet. class CaesarDecoder extends Converter { + /// The number of positions to shift each letter backward. final int shift; + /// Creates a decoder that reverses a Caesar cipher with [shift] positions. const CaesarDecoder(this.shift); @override @@ -136,24 +144,27 @@ class CaesarDecoder extends Converter { A `Codec` pairs an encoder (`Converter`) with a decoder (`Converter`). -Extend `Codec` and override the [`encoder`][] and [`decoder`][] getters: +Extend `Codec` and implement the [`encoder`][] and [`decoder`][] getters: + ```dart -/// A codec that encodes and decodes string using a +/// A codec that encodes and decodes strings using a /// [Caesar cipher](https://wikipedia.org/wiki/Caesar_cipher). class CaesarCodec extends Codec { + /// The number of positions to shift each letter. + final int shift; + @override - final CaesarEncoder encoder; + CaesarEncoder get encoder => CaesarEncoder(shift); @override - final CaesarDecoder decoder; + CaesarDecoder get decoder => CaesarDecoder(shift); - const CaesarCodec(int shift) - : encoder = CaesarEncoder(shift), - decoder = CaesarDecoder(shift); + /// Creates a Caesar cipher codec with the given [shift]. + const CaesarCodec(this.shift); /// Creates a [CaesarCodec] that uses ROT13 encoding. - const CaesarCodec.rot13() : this(13); + const CaesarCodec.rot13() : shift = 13; } ``` @@ -161,6 +172,7 @@ The codec inherits the `encode` and `decode` methods from `Codec` that delegate to the overriden encoder and decoder. It also inherits the `fuse` method and `inverted` getter: + ```dart void main() { const caesar = CaesarCodec(3); @@ -209,6 +221,7 @@ The chunked conversion then follows this sequence: To implement this in your converter, start by creating a sink class that performs the conversion: + ```dart /// A [StringConversionSink] that applies a Caesar cipher [_shift] and /// forwards the result to the [_output] sink. @@ -222,15 +235,16 @@ class _CaesarEncoderSink extends StringConversionSinkBase { void addSlice(String chunk, int start, int end, bool isLast) { final buffer = StringBuffer(); for (var i = start; i < end; i++) { - buffer.writeCharCode( - _shiftCodeUnit(chunk.codeUnitAt(i), _shift), - ); + buffer.writeCharCode(_shiftCodeUnit(chunk.codeUnitAt(i), _shift)); } _output.add(buffer.toString()); if (isLast) { _output.close(); } } + + @override + void close() => _output.close(); } ``` @@ -250,10 +264,14 @@ For byte-oriented converters, use `ByteConversionSink` instead. Next, override `startChunkedConversion` in your encoder to return an instance of your sink: + ```dart +/// Encodes a string by shifting each letter forward in the alphabet. class CaesarEncoder extends Converter { + /// The number of positions to shift each letter forward. final int shift; + /// Creates an encoder that shifts each letter by [shift] positions. const CaesarEncoder(this.shift); @override @@ -269,11 +287,13 @@ class CaesarEncoder extends Converter { StringConversionSink startChunkedConversion(Sink sink) { // Wrap the output sink if it isn't already // a StringConversionSink. + // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink : StringConversionSink.from(sink); return _CaesarEncoderSink(shift, stringSink); } + } ``` @@ -300,16 +320,18 @@ what's most useful as a `StreamTransformer`. Once your converters support chunked conversion, you can use them with the [`transform`][] method on `Stream`: + ```dart import 'dart:convert'; import 'dart:io'; +// ··· void main() async { const caesar = CaesarCodec(3); // Encrypt a file as a stream. final encrypted = File('message.txt') - .openRead() + .openRead() // .transform(utf8.decoder) .transform(caesar.encoder); @@ -322,10 +344,12 @@ void main() async { You can also compose your codec with others using [`fuse`][codec-fuse] to build data pipelines: + ```dart import 'dart:convert'; import 'dart:io'; +// ··· void main() async { const caesar = CaesarCodec(3); @@ -357,11 +381,13 @@ the same results as single-pass conversion. To test chunked conversion without setting up a stream, you can use the `ChunkedConversionSink.withCallback` factory constructor: + ```dart import 'dart:async'; import 'dart:convert'; import 'package:test/test.dart'; +import 'package:your_package/your_package.dart'; void main() { const codec = CaesarCodec(3); @@ -399,9 +425,7 @@ void main() { ); // Manually drive a chunked conversion. - final inputSink = codec.encoder.startChunkedConversion( - outputSink, - ); + final inputSink = codec.encoder.startChunkedConversion(outputSink); inputSink.add('hel'); inputSink.add('lo '); inputSink.add('world'); @@ -415,9 +439,7 @@ void main() { // Split the input into chunks and convert as a stream. final stream = Stream.fromIterable(['hel', 'lo ', 'world']); - final result = await stream - .transform(codec.encoder) - .join(); + final result = await stream.transform(codec.encoder).join(); expect(result, equals(codec.encode(input))); }); @@ -430,10 +452,12 @@ void main() { The following example brings together all the pieces from this guide, implementing a complete Caesar cipher codec: + ```dart import 'dart:convert'; -/// Shifts the specified [codeUnit] by [shift] positions in the alphabet. +/// Shifts the specified [codeUnit] by +/// [shift] positions in the alphabet. /// /// Only shifts lowercase ASCII letters (a-z). /// All other characters are returned unchanged. @@ -458,21 +482,24 @@ class _CaesarEncoderSink extends StringConversionSinkBase { void addSlice(String chunk, int start, int end, bool isLast) { final buffer = StringBuffer(); for (var i = start; i < end; i++) { - buffer.writeCharCode( - _shiftCodeUnit(chunk.codeUnitAt(i), _shift), - ); + buffer.writeCharCode(_shiftCodeUnit(chunk.codeUnitAt(i), _shift)); } _output.add(buffer.toString()); if (isLast) { _output.close(); } } + + @override + void close() => _output.close(); } /// Encodes a string by shifting each letter forward in the alphabet. class CaesarEncoder extends Converter { + /// The number of positions to shift each letter forward. final int shift; + /// Creates an encoder that shifts each letter by [shift] positions. const CaesarEncoder(this.shift); @override @@ -486,6 +513,7 @@ class CaesarEncoder extends Converter { @override StringConversionSink startChunkedConversion(Sink sink) { + // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink : StringConversionSink.from(sink); @@ -496,8 +524,10 @@ class CaesarEncoder extends Converter { /// Decodes a Caesar-cipher-encoded string /// by shifting each letter backward in the alphabet. class CaesarDecoder extends Converter { + /// The number of positions to shift each letter backward. final int shift; + /// Creates a decoder that reverses a Caesar cipher with [shift] positions. const CaesarDecoder(this.shift); @override @@ -511,6 +541,7 @@ class CaesarDecoder extends Converter { @override StringConversionSink startChunkedConversion(Sink sink) { + // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink : StringConversionSink.from(sink); @@ -519,20 +550,22 @@ class CaesarDecoder extends Converter { } /// A codec that encodes and decodes strings using a -/// [Caesar cipher](https://en.wikipedia.org/wiki/Caesar_cipher). +/// [Caesar cipher](https://wikipedia.org/wiki/Caesar_cipher). class CaesarCodec extends Codec { + /// The number of positions to shift each letter. + final int shift; + @override - final CaesarEncoder encoder; + CaesarEncoder get encoder => CaesarEncoder(shift); @override - final CaesarDecoder decoder; + CaesarDecoder get decoder => CaesarDecoder(shift); - const CaesarCodec(int shift) - : encoder = CaesarEncoder(shift), - decoder = CaesarDecoder(shift); + /// Creates a Caesar cipher codec with the given [shift]. + const CaesarCodec(this.shift); /// Creates a [CaesarCodec] that uses ROT13 encoding. - const CaesarCodec.rot13() : this(13); + const CaesarCodec.rot13() : shift = 13; } void main() { @@ -664,6 +697,7 @@ might be an appropriate starting point before optimizing. Following the convention of the built-in codecs, consider exposing commonly used configurations as `const` top-level instances: + ```dart /// A [CaesarCodec] with the standard shift of 13 (ROT13). const rot13 = CaesarCodec.rot13(); diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index 7a94284b63..822dc895f0 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -5,6 +5,9 @@ description: >- to encode, decode, and transform data. --- + + + The [`dart:convert`][] library provides a framework for encoding, decoding, and transforming data in Dart. At its core are two abstractions: @@ -105,6 +108,7 @@ a stream transformer that splits strings into individual lines. Every codec provides [`encode`][] and [`decode`][] convenience methods. These delegate to the codec's [`encoder`][] and [`decoder`][] converters: + ```dart import 'dart:convert'; @@ -127,6 +131,7 @@ such as [`base64Decode`][] being a shorthand for [`base64.decode`][]. You can also use the [`encoder`][] and [`decoder`][] properties to access the underlying converters: + ```dart import 'dart:convert'; @@ -144,6 +149,7 @@ a default configuration of the underlying converters. If you want to customize them, you can create custom instances of the converters and codecs: + ```dart import 'dart:convert'; @@ -182,11 +188,12 @@ One of the most powerful features of codecs is composition. The [`fuse`][codec-fuse] method on `Codec` combines two codecs into one, potentially eliminating the intermediate representation: + ```dart import 'dart:convert'; // Create a codec that goes directly from Dart objects to UTF-8 bytes. -final jsonUtf8 = json.fuse(utf8); +final Codec> jsonUtf8 = json.fuse(utf8); void main() { // Encode directly to bytes without an intermediate string. @@ -210,6 +217,7 @@ than encoding in two separate steps. Converters also have a [`fuse`][converter-fuse] method for composing one-way transformations: + ```dart import 'dart:convert'; @@ -234,6 +242,7 @@ that shouldn't be loaded entirely into memory at once. Use the [`transform`][] method to apply a converter to a stream: + ```dart import 'dart:convert'; import 'dart:io'; @@ -242,9 +251,9 @@ void main() async { // Read a file as a stream of bytes, decode UTF-8 into a string, // then split the single string into lines. final lines = File('data.txt') - .openRead() - .transform(utf8.decoder) - .transform(const LineSplitter()); + .openRead() // + .transform(utf8.decoder) + .transform(const LineSplitter()); await for (final line in lines) { print(line); @@ -255,6 +264,7 @@ void main() async { You can build longer transformating pipelines by chaining multiple `transform` calls or by fusing converters first: + ```dart import 'dart:convert'; import 'dart:io'; @@ -262,9 +272,9 @@ import 'dart:io'; void main() async { // Read a JSON file and decode it in a single streaming pipeline. final stream = File('data.json') - .openRead() - .transform(utf8.decoder) - .transform(json.decoder); + .openRead() // + .transform(utf8.decoder) + .transform(json.decoder); await for (final value in stream) { print(value); @@ -276,16 +286,17 @@ Streaming conversion is especially useful with compression. The following example decompresses and decodes a gzipped text file without loading the entire file into memory: + ```dart import 'dart:convert'; import 'dart:io'; void main() async { final lines = File('log.gz') - .openRead() - .transform(gzip.decoder) - .transform(utf8.decoder) - .transform(const LineSplitter()); + .openRead() + .transform(gzip.decoder) + .transform(utf8.decoder) + .transform(const LineSplitter()); await for (final line in lines) { print(line); @@ -303,6 +314,7 @@ where its subclasses encode and decode strings to lists of bytes. It adds a `name` property and a static `getByName` method for looking up built-in, supported encodings by their [IANA name][]: + ```dart import 'dart:convert'; @@ -327,6 +339,7 @@ In contrast to `Stream.transform` which returns a `Stream`, the `decodeStream` method reads a byte stream and returns a single decoded `String` (wrapped in a `Future`). + ```dart import 'dart:convert'; import 'dart:io'; @@ -343,6 +356,7 @@ Many `dart:io` APIs accept an `Encoding` parameter, making it straightforward to read and write files with different character encodings: + ```dart import 'dart:convert'; import 'dart:io'; From f916ba97f718c0459dae3a517e6ac5ccf0be975b Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 23:19:29 +0800 Subject: [PATCH 06/11] Add highlighting to the code excerpts --- .../lib/convert/build_custom_codecs.dart | 36 +++-- .../convert/build_custom_codecs_complete.dart | 45 +++--- .../convert/build_custom_codecs_stream.dart | 8 +- .../libraries/convert/build-custom-codecs.md | 128 ++++++++++-------- .../convert/converters-and-codecs.md | 22 +-- 5 files changed, 128 insertions(+), 111 deletions(-) diff --git a/examples/libraries/lib/convert/build_custom_codecs.dart b/examples/libraries/lib/convert/build_custom_codecs.dart index b7949341ff..bef9f632c8 100644 --- a/examples/libraries/lib/convert/build_custom_codecs.dart +++ b/examples/libraries/lib/convert/build_custom_codecs.dart @@ -28,7 +28,9 @@ class _CaesarEncoderSink extends StringConversionSinkBase { // #docregion basic-encoder, caesar-encoder /// Encodes a string by shifting each letter forward in the alphabet. +// #docregion chunked-encoder class CaesarEncoder extends Converter { + // #enddocregion chunked-encoder /// The number of positions to shift each letter forward. final int shift; @@ -45,7 +47,8 @@ class CaesarEncoder extends Converter { } // #enddocregion basic-encoder - // #docregion start-chunked-conversion + // #docregion chunked-encoder + @override StringConversionSink startChunkedConversion(Sink sink) { // Wrap the output sink if it isn't already @@ -57,10 +60,9 @@ class CaesarEncoder extends Converter { return _CaesarEncoderSink(shift, stringSink); } - // #enddocregion start-chunked-conversion // #docregion basic-encoder } -// #enddocregion caesar-encoder +// #enddocregion caesar-encoder, chunked-encoder // #docregion shift-code-unit /// Shifts the specified [codeUnit] by @@ -81,7 +83,9 @@ int _shiftCodeUnit(int codeUnit, int shift) { // #docregion basic-decoder /// Decodes a Caesar-cipher-encoded string by /// shifting each letter backward in the alphabet. +// #docregion chunked-decoder class CaesarDecoder extends Converter { + //#enddocregion chunked-decoder /// The number of positions to shift each letter backward. final int shift; @@ -99,8 +103,12 @@ class CaesarDecoder extends Converter { } // #enddocregion basic-decoder + // #docregion chunked-decoder + @override StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink @@ -110,7 +118,7 @@ class CaesarDecoder extends Converter { // #docregion basic-decoder } -// #enddocregion basic-decoder +// #enddocregion basic-decoder, chunked-decoder // #docregion caesar-codec /// A codec that encodes and decodes strings using a @@ -119,17 +127,17 @@ class CaesarCodec extends Codec { /// The number of positions to shift each letter. final int shift; - @override - CaesarEncoder get encoder => CaesarEncoder(shift); - - @override - CaesarDecoder get decoder => CaesarDecoder(shift); - /// Creates a Caesar cipher codec with the given [shift]. const CaesarCodec(this.shift); /// Creates a [CaesarCodec] that uses ROT13 encoding. const CaesarCodec.rot13() : shift = 13; + + @override + CaesarEncoder get encoder => CaesarEncoder(shift); + + @override + CaesarDecoder get decoder => CaesarDecoder(shift); } // #enddocregion caesar-codec @@ -143,17 +151,17 @@ void useEncoderExample() { // #docregion use-codec void useCodecExample() { - const caesar = CaesarCodec(3); + const cipher = CaesarCodec(3); - final encoded = caesar.encode('hello'); + final encoded = cipher.encode('hello'); print(encoded); // khoor - final decoded = caesar.decode(encoded); + final decoded = cipher.decode(encoded); print(decoded); // hello // The `inverted` getter returns a new codec that // applies converts in the inverse direction of the codec. - final inverted = caesar.inverted; + final inverted = cipher.inverted; print(inverted.encode('khoor')); // hello } // #enddocregion use-codec diff --git a/examples/libraries/lib/convert/build_custom_codecs_complete.dart b/examples/libraries/lib/convert/build_custom_codecs_complete.dart index 497cf7d74d..8bd211a1ca 100644 --- a/examples/libraries/lib/convert/build_custom_codecs_complete.dart +++ b/examples/libraries/lib/convert/build_custom_codecs_complete.dart @@ -1,20 +1,5 @@ -// #docregion complete import 'dart:convert'; -/// Shifts the specified [codeUnit] by -/// [shift] positions in the alphabet. -/// -/// Only shifts lowercase ASCII letters (a-z). -/// All other characters are returned unchanged. -int _shiftCodeUnit(int codeUnit, int shift) { - const a = 0x61; - const z = 0x7A; - if (codeUnit >= a && codeUnit <= z) { - return a + (codeUnit - a + shift) % 26; - } - return codeUnit; -} - /// A [StringConversionSink] that applies a Caesar cipher shift /// and forwards the result to [_output]. class _CaesarEncoderSink extends StringConversionSinkBase { @@ -58,6 +43,8 @@ class CaesarEncoder extends Converter { @override StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink @@ -86,6 +73,8 @@ class CaesarDecoder extends Converter { @override StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink @@ -100,17 +89,17 @@ class CaesarCodec extends Codec { /// The number of positions to shift each letter. final int shift; - @override - CaesarEncoder get encoder => CaesarEncoder(shift); - - @override - CaesarDecoder get decoder => CaesarDecoder(shift); - /// Creates a Caesar cipher codec with the given [shift]. const CaesarCodec(this.shift); /// Creates a [CaesarCodec] that uses ROT13 encoding. const CaesarCodec.rot13() : shift = 13; + + @override + CaesarEncoder get encoder => CaesarEncoder(shift); + + @override + CaesarDecoder get decoder => CaesarDecoder(shift); } void main() { @@ -123,4 +112,16 @@ void main() { print(decoded); // the quick brown fox } -// #enddocregion complete +/// Shifts the specified [codeUnit] by +/// [shift] positions in the alphabet. +/// +/// Only shifts lowercase ASCII letters (a-z). +/// All other characters are returned unchanged. +int _shiftCodeUnit(int codeUnit, int shift) { + const a = 0x61; + const z = 0x7A; + if (codeUnit >= a && codeUnit <= z) { + return a + (codeUnit - a + shift) % 26; + } + return codeUnit; +} diff --git a/examples/libraries/lib/convert/build_custom_codecs_stream.dart b/examples/libraries/lib/convert/build_custom_codecs_stream.dart index 703511db2e..c121245e57 100644 --- a/examples/libraries/lib/convert/build_custom_codecs_stream.dart +++ b/examples/libraries/lib/convert/build_custom_codecs_stream.dart @@ -8,13 +8,13 @@ import 'build_custom_codecs.dart'; // #docregion stream-transform void streamTransformExample() async { - const caesar = CaesarCodec(3); + const cipher = CaesarCodec(3); // Encrypt a file as a stream. final encrypted = File('message.txt') .openRead() // .transform(utf8.decoder) - .transform(caesar.encoder); + .transform(cipher.encoder); await for (final chunk in encrypted) { stdout.write(chunk); @@ -24,10 +24,10 @@ void streamTransformExample() async { // #docregion fuse-pipeline void fusePipelineExample() async { - const caesar = CaesarCodec(3); + const cipher = CaesarCodec(3); // Create a codec that encrypts, then compresses. - final encryptAndCompress = caesar.fuse(utf8).fuse(gzip); + final encryptAndCompress = cipher.fuse(utf8).fuse(gzip); // Write encrypted, compressed data. final output = File('message.gz').openWrite(); diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md index 0ff618e7de..c955503ba5 100644 --- a/src/content/libraries/convert/build-custom-codecs.md +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -118,7 +118,7 @@ but creating a separate class keeps the intent clear and lets you customize behavior if needed: -```dart +```dart highlightLines=14-15 /// Decodes a Caesar-cipher-encoded string by /// shifting each letter backward in the alphabet. class CaesarDecoder extends Converter { @@ -147,24 +147,24 @@ with a decoder (`Converter`). Extend `Codec` and implement the [`encoder`][] and [`decoder`][] getters: -```dart +```dart highlightLines=14,17 /// A codec that encodes and decodes strings using a /// [Caesar cipher](https://wikipedia.org/wiki/Caesar_cipher). class CaesarCodec extends Codec { /// The number of positions to shift each letter. final int shift; - @override - CaesarEncoder get encoder => CaesarEncoder(shift); - - @override - CaesarDecoder get decoder => CaesarDecoder(shift); - /// Creates a Caesar cipher codec with the given [shift]. const CaesarCodec(this.shift); /// Creates a [CaesarCodec] that uses ROT13 encoding. const CaesarCodec.rot13() : shift = 13; + + @override + CaesarEncoder get encoder => CaesarEncoder(shift); + + @override + CaesarDecoder get decoder => CaesarDecoder(shift); } ``` @@ -175,17 +175,17 @@ It also inherits the `fuse` method and `inverted` getter: ```dart void main() { - const caesar = CaesarCodec(3); + const cipher = CaesarCodec(3); - final encoded = caesar.encode('hello'); + final encoded = cipher.encode('hello'); print(encoded); // khoor - final decoded = caesar.decode(encoded); + final decoded = cipher.decode(encoded); print(decoded); // hello // The `inverted` getter returns a new codec that // applies converts in the inverse direction of the codec. - final inverted = caesar.inverted; + final inverted = cipher.inverted; print(inverted.encode('khoor')); // hello } ``` @@ -264,24 +264,10 @@ For byte-oriented converters, use `ByteConversionSink` instead. Next, override `startChunkedConversion` in your encoder to return an instance of your sink: - -```dart -/// Encodes a string by shifting each letter forward in the alphabet. + +```dart highlightLines=4-13 class CaesarEncoder extends Converter { - /// The number of positions to shift each letter forward. - final int shift; - - /// Creates an encoder that shifts each letter by [shift] positions. - const CaesarEncoder(this.shift); - - @override - String convert(String input) { - final buffer = StringBuffer(); - for (final codeUnit in input.codeUnits) { - buffer.writeCharCode(_shiftCodeUnit(codeUnit, shift)); - } - return buffer.toString(); - } + // ··· @override StringConversionSink startChunkedConversion(Sink sink) { @@ -300,6 +286,25 @@ class CaesarEncoder extends Converter { Apply the same approach to `CaesarDecoder`, using `26 - shift` as the shift value in its sink. + +```dart highlightLines=4-13 +class CaesarDecoder extends Converter { + // ··· + + @override + StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. + // ignore: close_sinks + final stringSink = sink is StringConversionSink + ? sink + : StringConversionSink.from(sink); + return _CaesarEncoderSink(26 - shift, stringSink); + } + +} +``` + With chunked conversion implemented, the converter automatically works as a `StreamTransformer` through the inherited `bind` method. @@ -321,19 +326,19 @@ Once your converters support chunked conversion, you can use them with the [`transform`][] method on `Stream`: -```dart +```dart highlightLines=9-12 import 'dart:convert'; import 'dart:io'; // ··· void main() async { - const caesar = CaesarCodec(3); + const cipher = CaesarCodec(3); // Encrypt a file as a stream. final encrypted = File('message.txt') .openRead() // .transform(utf8.decoder) - .transform(caesar.encoder); + .transform(cipher.encoder); await for (final chunk in encrypted) { stdout.write(chunk); @@ -344,17 +349,16 @@ void main() async { You can also compose your codec with others using [`fuse`][codec-fuse] to build data pipelines: - -```dart + +```dart highlightLines=8 import 'dart:convert'; import 'dart:io'; -// ··· void main() async { - const caesar = CaesarCodec(3); + const cipher = CaesarCodec(3); // Create a codec that encrypts, then compresses. - final encryptAndCompress = caesar.fuse(utf8).fuse(gzip); + final encryptAndCompress = cipher.fuse(utf8).fuse(gzip); // Write encrypted, compressed data. final output = File('message.gz').openWrite(); @@ -452,24 +456,10 @@ void main() { The following example brings together all the pieces from this guide, implementing a complete Caesar cipher codec: - + ```dart import 'dart:convert'; -/// Shifts the specified [codeUnit] by -/// [shift] positions in the alphabet. -/// -/// Only shifts lowercase ASCII letters (a-z). -/// All other characters are returned unchanged. -int _shiftCodeUnit(int codeUnit, int shift) { - const a = 0x61; - const z = 0x7A; - if (codeUnit >= a && codeUnit <= z) { - return a + (codeUnit - a + shift) % 26; - } - return codeUnit; -} - /// A [StringConversionSink] that applies a Caesar cipher shift /// and forwards the result to [_output]. class _CaesarEncoderSink extends StringConversionSinkBase { @@ -513,6 +503,8 @@ class CaesarEncoder extends Converter { @override StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink @@ -541,6 +533,8 @@ class CaesarDecoder extends Converter { @override StringConversionSink startChunkedConversion(Sink sink) { + // Wrap the output sink if it isn't already + // a StringConversionSink. // ignore: close_sinks final stringSink = sink is StringConversionSink ? sink @@ -555,17 +549,17 @@ class CaesarCodec extends Codec { /// The number of positions to shift each letter. final int shift; - @override - CaesarEncoder get encoder => CaesarEncoder(shift); - - @override - CaesarDecoder get decoder => CaesarDecoder(shift); - /// Creates a Caesar cipher codec with the given [shift]. const CaesarCodec(this.shift); /// Creates a [CaesarCodec] that uses ROT13 encoding. const CaesarCodec.rot13() : shift = 13; + + @override + CaesarEncoder get encoder => CaesarEncoder(shift); + + @override + CaesarDecoder get decoder => CaesarDecoder(shift); } void main() { @@ -577,6 +571,20 @@ void main() { final decoded = codec.decode(encoded); print(decoded); // the quick brown fox } + +/// Shifts the specified [codeUnit] by +/// [shift] positions in the alphabet. +/// +/// Only shifts lowercase ASCII letters (a-z). +/// All other characters are returned unchanged. +int _shiftCodeUnit(int codeUnit, int shift) { + const a = 0x61; + const z = 0x7A; + if (codeUnit >= a && codeUnit <= z) { + return a + (codeUnit - a + shift) % 26; + } + return codeUnit; +} ``` ## Design considerations @@ -600,7 +608,7 @@ and the `encode` and `decode` methods (for per-call overrides). where its constructor accepts a default `reviver`, and its `decode` method accepts one that overrides it per call: -```dart +```dart highlightLines=5,10-12,15-17 class JsonCodec extends Codec { // ... @@ -698,7 +706,7 @@ Following the convention of the built-in codecs, consider exposing commonly used configurations as `const` top-level instances: -```dart +```dart highlightLines=2 /// A [CaesarCodec] with the standard shift of 13 (ROT13). const rot13 = CaesarCodec.rot13(); ``` diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index 822dc895f0..1caf1b7174 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -109,7 +109,7 @@ Every codec provides [`encode`][] and [`decode`][] convenience methods. These delegate to the codec's [`encoder`][] and [`decoder`][] converters: -```dart +```dart highlightLines=5,9 import 'dart:convert'; void main() { @@ -132,7 +132,7 @@ You can also use the [`encoder`][] and [`decoder`][] properties to access the underlying converters: -```dart +```dart highlightLines=4,7 import 'dart:convert'; void main() { @@ -150,7 +150,7 @@ If you want to customize them, you can create custom instances of the converters and codecs: -```dart +```dart highlightLines=5-6,14-15 import 'dart:convert'; void main() { @@ -189,7 +189,7 @@ The [`fuse`][codec-fuse] method on `Codec` combines two codecs into one, potentially eliminating the intermediate representation: -```dart +```dart highlightLines=4,8,12 import 'dart:convert'; // Create a codec that goes directly from Dart objects to UTF-8 bytes. @@ -218,7 +218,7 @@ Converters also have a [`fuse`][converter-fuse] method for composing one-way transformations: -```dart +```dart highlightLines=5-6 import 'dart:convert'; void main() { @@ -243,7 +243,7 @@ that shouldn't be loaded entirely into memory at once. Use the [`transform`][] method to apply a converter to a stream: -```dart +```dart highlightLines=7-10 import 'dart:convert'; import 'dart:io'; @@ -265,7 +265,7 @@ You can build longer transformating pipelines by chaining multiple `transform` calls or by fusing converters first: -```dart +```dart highlightLines=6-9 import 'dart:convert'; import 'dart:io'; @@ -287,7 +287,7 @@ The following example decompresses and decodes a gzipped text file without loading the entire file into memory: -```dart +```dart highlightLines=5-9 import 'dart:convert'; import 'dart:io'; @@ -315,7 +315,7 @@ It adds a `name` property and a static `getByName` method for looking up built-in, supported encodings by their [IANA name][]: -```dart +```dart highlightLines=4 import 'dart:convert'; void main() { @@ -340,7 +340,7 @@ the `decodeStream` method reads a byte stream and returns a single decoded `String` (wrapped in a `Future`). -```dart +```dart highlightLines=6-7 import 'dart:convert'; import 'dart:io'; @@ -357,7 +357,7 @@ making it straightforward to read and write files with different character encodings: -```dart +```dart highlightLines=7 import 'dart:convert'; import 'dart:io'; From 699add7bf8a2a016a475daa2182c3a1061f32d70 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 23:30:26 +0800 Subject: [PATCH 07/11] Add references to the old dart:convert page --- src/content/libraries/dart-convert.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/content/libraries/dart-convert.md b/src/content/libraries/dart-convert.md index bf9a8d0b62..d9ceda16b5 100644 --- a/src/content/libraries/dart-convert.md +++ b/src/content/libraries/dart-convert.md @@ -26,6 +26,14 @@ To use this library, import dart:convert. import 'dart:convert'; ``` +:::tip +For a deeper look at the codec and converter framework, +including streaming, composition with `fuse`, and character encodings, +check out [Use converters and codecs][]. + +To get started building your own codecs, +check out [Build custom codecs and converters][]. +::: ## Decoding and encoding JSON @@ -139,6 +147,17 @@ for (int i = 0; i < encoded.length; i++) { The dart:convert library also has converters for ASCII and ISO-8859-1 (Latin1). For details, see the [API reference for the dart:convert library.][dart:convert] +## Learn more + +To go beyond basic encoding and decoding, check out the following guides: + +- [Use converters and codecs][]: Learn about more of the built-in codecs, + how to compose them with `fuse`, and how to use them with streams. +- [Build custom codecs and converters][]: Implement your own `Codec` + and `Converter` classes with support for streaming and composition. + [JSON]: https://www.json.org/ [UTF-8]: https://en.wikipedia.org/wiki/UTF-8 [dart:convert]: {{site.dart-api}}/dart-convert/dart-convert-library.html +[Use converters and codecs]: /libraries/convert/converters-and-codecs +[Build custom codecs and converters]: /libraries/convert/build-custom-codecs From e3b716fa7ea9732359f55fd48576e00fd47916e2 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 23:34:19 +0800 Subject: [PATCH 08/11] Adjust decision tree copy --- src/content/libraries/convert/converters-and-codecs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index 1caf1b7174..d4ec453dac 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -376,7 +376,7 @@ void main() async { For serialization of application-level data and objects, where you want to convert between Dart model classes and JSON, -the `fromJson`/`toJson` convention supported by packages such as +the `fromJson` and `toJson` convention supported by packages such as [`package:json_serializable`][] is the idiomatic approach in Dart apps. These tools generate simple factory constructors and methods that work directly with `Map`. @@ -392,7 +392,7 @@ such as with bytes, strings, or compressed data, the codec pattern is a natural fit. If the conversion is about how data is _structured_, such as mapping JSON fields to Dart class properties, -use `fromJson`/`toJson`. +use a pattern like `fromJson` and `toJson`. If you do want to build your own codecs and converters, check out [Build custom codecs and converters][]. From dd2496f9c0bf97285bc614c1ce15bcc4bb6178cb Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 23:43:45 +0800 Subject: [PATCH 09/11] Add additional cross links --- .../libraries/convert/build-custom-codecs.md | 41 +++++++++++++------ .../convert/converters-and-codecs.md | 5 ++- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md index c955503ba5..0ed336b0ef 100644 --- a/src/content/libraries/convert/build-custom-codecs.md +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -45,8 +45,8 @@ Build a custom `Codec` or `Converter` when your conversion: For one-directional or internal conversions, a purpose-built function is often simpler. -For application-level JSON serialization, -the `fromJson`/`toJson` convention with code generation is more idiomatic. +For application-level object serialization, +the `fromJson` and `toJson` convention with code generation is more idiomatic. For more insight on when to use each approach, check out [Codecs versus `fromJson` and `toJson`][when-to-use-codecs]. @@ -202,7 +202,7 @@ enabling consumers to use the codec with the [`Stream.transform`][] method. The basic `convert` method processes all input at once. To support streaming, override the -`startChunkedConversion` method in your converter. +[`startChunkedConversion`][] method in your converter. This method receives a downstream output sink and returns an upstream input sink. The chunked conversion then follows this sequence: @@ -255,10 +255,10 @@ When `isLast` is `true`, the sink closes the output without requiring a separate `close` call. :::note -`StringConversionSink` is a base class that +[`StringConversionSink`][] is a base class that implements `add`, `addSlice`, and `close` in terms of each other. -You only need to override `addSlice()`. -For byte-oriented converters, use `ByteConversionSink` instead. +You only need to override the `addSlice` method. +For byte-oriented converters, use [`ByteConversionSink`][] instead. ::: Next, override `startChunkedConversion` in your encoder @@ -306,20 +306,28 @@ class CaesarDecoder extends Converter { ``` With chunked conversion implemented, -the converter automatically works as a `StreamTransformer` -through the inherited `bind` method. +the converter automatically works as a [`StreamTransformer`][] +through the inherited [`bind`][] method. + +[`startChunkedConversion`]: {{site.dart-api}}/dart-convert/Converter/startChunkedConversion.html +[`StringConversionSink`]: {{site.dart-api}}/dart-convert/StringConversionSink-class.html +[`ByteConversionSink`]: {{site.dart-api}}/dart-convert/ByteConversionSink-class.html +[`StreamTransformer`]: {{site.dart-api}}/dart-async/StreamTransformer-class.html +[`bind`]: {{site.dart-api}}/dart-async/StreamTransformer/bind.html ### Chunked conversion type versus synchronous type The chunked (streaming) type signature of a converter doesn't always match the synchronous `convert` signature. -For example, `LineSplitter` synchronously converts +For example, [`LineSplitter`][] synchronously converts all lines in a `String` to a `List` at once, but in chunked mode it converts `String` chunks to `String` chunks where each output chunk is a single line. The chunked type is determined by what's most useful as a `StreamTransformer`. +[`LineSplitter`]: {{site.dart-api}}/dart-convert/LineSplitter-class.html + ## Use your codec with streams Once your converters support chunked conversion, @@ -383,7 +391,7 @@ that edge cases are handled, and that chunked conversion produces the same results as single-pass conversion. To test chunked conversion without setting up a stream, -you can use the `ChunkedConversionSink.withCallback` factory constructor: +you can use the [`ChunkedConversionSink.withCallback`][] factory constructor: ```dart @@ -451,6 +459,8 @@ void main() { } ``` +[`ChunkedConversionSink.withCallback`]: {{site.dart-api}}/dart-convert/ChunkedConversionSink/ChunkedConversionSink.withCallback.html + ## Complete example The following example brings together all the pieces from this guide, @@ -604,7 +614,7 @@ and follows the pattern set by the built-in codecs. If a codec supports configuration options, add named parameters to both the constructor (for defaults) and the `encode` and `decode` methods (for per-call overrides). -`JsonCodec` demonstrates this pattern, +[`JsonCodec`][] demonstrates this pattern, where its constructor accepts a default `reviver`, and its `decode` method accepts one that overrides it per call: @@ -631,14 +641,19 @@ class JsonCodec extends Codec { } ``` +[`JsonCodec`]: {{site.dart-api}}/dart-convert/JsonCodec-class.html + ### Override fuse for optimized composition If your codec is commonly composed with another specific codec, -override the `fuse` method to return an optimized implementation. -The built-in `JsonCodec` does this: when fused with `Utf8Codec`, +override the [`fuse`][] method to return an optimized implementation. +The built-in [`JsonCodec`][] does this: when fused with [`Utf8Codec`][], it returns a codec that uses [`JsonUtf8Encoder`][] to bypass the intermediate string representation. +[`fuse`]: {{site.dart-api}}/dart-convert/Codec/fuse.html +[`JsonCodec`]: {{site.dart-api}}/dart-convert/JsonCodec-class.html +[`Utf8Codec`]: {{site.dart-api}}/dart-convert/Utf8Codec-class.html [`JsonUtf8Encoder`]: {{site.dart-api}}/dart-convert/JsonUtf8Encoder-class.html ### Choose the right sink base class diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index d4ec453dac..5bca1836a7 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -309,7 +309,7 @@ void main() async { ## Character encodings -The `Encoding` base class is specialized for character-encoding codecs, +The [`Encoding`][] base class is specialized for character-encoding codecs, where its subclasses encode and decode strings to lists of bytes. It adds a `name` property and a static `getByName` method for looking up built-in, supported encodings by their [IANA name][]: @@ -368,6 +368,7 @@ void main() async { } ``` +[`Encoding`]: {{site.dart-api}}/dart-convert/Encoding-class.html [`decodeStream`]: {{site.dart-api}}/dart-convert/Encoding/decodeStream.html [`package:charset`]: {{site.pub-pkg}}/charset [iana name]: https://www.iana.org/assignments/character-sets/character-sets.xhtml @@ -385,7 +386,7 @@ The `Codec` and `Converter` classes serve a different purpose. They're designed for data format transformations: encoding schemes, compression, character sets, and other conversions where streaming support, bidirectional symmetry, -and composability through `fuse()` matter. +and composability through `fuse` matter. If the conversion is about how data is _represented_, such as with bytes, strings, or compressed data, From 674ca75751b110ee09dc508df0c145cda5e17b00 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 23:48:47 +0800 Subject: [PATCH 10/11] Small fixes --- examples/libraries/lib/convert/build_custom_codecs.dart | 2 +- src/content/libraries/convert/build-custom-codecs.md | 6 +++--- src/content/libraries/convert/converters-and-codecs.md | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/libraries/lib/convert/build_custom_codecs.dart b/examples/libraries/lib/convert/build_custom_codecs.dart index bef9f632c8..a3ad9aac79 100644 --- a/examples/libraries/lib/convert/build_custom_codecs.dart +++ b/examples/libraries/lib/convert/build_custom_codecs.dart @@ -160,7 +160,7 @@ void useCodecExample() { print(decoded); // hello // The `inverted` getter returns a new codec that - // applies converts in the inverse direction of the codec. + // converts in the inverse direction of the original codec. final inverted = cipher.inverted; print(inverted.encode('khoor')); // hello } diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md index 0ed336b0ef..6c2d4fe66c 100644 --- a/src/content/libraries/convert/build-custom-codecs.md +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -169,7 +169,7 @@ class CaesarCodec extends Codec { ``` The codec inherits the `encode` and `decode` methods from `Codec` -that delegate to the overriden encoder and decoder. +that delegate to the overridden encoder and decoder. It also inherits the `fuse` method and `inverted` getter: @@ -184,7 +184,7 @@ void main() { print(decoded); // hello // The `inverted` getter returns a new codec that - // applies converts in the inverse direction of the codec. + // converts in the inverse direction of the original codec. final inverted = cipher.inverted; print(inverted.encode('khoor')); // hello } @@ -735,7 +735,7 @@ This makes the codec easy to discover and use. - For examples of well-implemented codecs and converters, such as `HexCodec` and its `HexEncoder` and `HexDecoder`, reference the implementation of [`package:convert`][]. -- If you haven't yet, read the [Converters and codecs][] for +- If you haven't yet, check out [Converters and codecs][] for an introduction to using the built-in codecs and converters. [library-api-docs]: {{site.dart-api}}/dart-convert diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index 5bca1836a7..f065e83089 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -101,8 +101,6 @@ a stream transformer that splits strings into individual lines. [`HtmlEscape`]: {{site.dart-api}}/dart-convert/HtmlEscape-class.html [`LineSplitter`]: {{site.dart-api}}/dart-convert/LineSplitter-class.html -## Built-in converters - ## Encode and decode data Every codec provides [`encode`][] and [`decode`][] convenience methods. @@ -261,7 +259,7 @@ void main() async { } ``` -You can build longer transformating pipelines by +You can build longer transformation pipelines by chaining multiple `transform` calls or by fusing converters first: From d833effe8388c3c0793b3c46f67e27df73ae4a81 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Mon, 16 Mar 2026 23:53:07 +0800 Subject: [PATCH 11/11] Standardize name of article --- src/content/libraries/convert/build-custom-codecs.md | 8 ++++---- src/content/libraries/convert/converters-and-codecs.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content/libraries/convert/build-custom-codecs.md b/src/content/libraries/convert/build-custom-codecs.md index 6c2d4fe66c..869bb7328d 100644 --- a/src/content/libraries/convert/build-custom-codecs.md +++ b/src/content/libraries/convert/build-custom-codecs.md @@ -21,7 +21,7 @@ shifts each letter by a fixed number of positions in the alphabet. :::tip To learn more about codecs and converters and the built-in ones, -check out [Converters and codecs][]. +check out [Use Converters and codecs][]. ::: [`dart:convert`]: {{site.dart-api}}/dart-convert @@ -31,7 +31,7 @@ check out [Converters and codecs][]. [Caesar cipher]: https://wikipedia.org/wiki/Caesar_cipher -[Converters and codecs]: /libraries/convert/converters-and-codecs +[Use converters and codecs]: /libraries/convert/converters-and-codecs ## When to build a custom codec @@ -735,9 +735,9 @@ This makes the codec easy to discover and use. - For examples of well-implemented codecs and converters, such as `HexCodec` and its `HexEncoder` and `HexDecoder`, reference the implementation of [`package:convert`][]. -- If you haven't yet, check out [Converters and codecs][] for +- If you haven't yet, check out [Use converters and codecs][] for an introduction to using the built-in codecs and converters. [library-api-docs]: {{site.dart-api}}/dart-convert [`package:convert`]: {{site.pub-pkg}}/convert -[Converters and codecs]: /libraries/convert/converters-and-codecs +[Use converters and codecs]: /libraries/convert/converters-and-codecs diff --git a/src/content/libraries/convert/converters-and-codecs.md b/src/content/libraries/convert/converters-and-codecs.md index f065e83089..7d99d72d3f 100644 --- a/src/content/libraries/convert/converters-and-codecs.md +++ b/src/content/libraries/convert/converters-and-codecs.md @@ -1,5 +1,5 @@ --- -title: Converters and codecs +title: Use converters and codecs description: >- Learn how to use Dart's codec and converter classes to encode, decode, and transform data.