diff --git a/.gitignore b/.gitignore
index 86c063c..cedb7b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@ runtime.*.liblouis/runtimes/
# Rider / ReSharper per-user settings
*.DotSettings.user
+
+# Worktrees created by spawned Claude Code sessions
+.claude/worktrees/
diff --git a/LibLouis.NET.Test/AssemblyInfo.cs b/LibLouis.NET.Test/AssemblyInfo.cs
new file mode 100644
index 0000000..dffc93b
--- /dev/null
+++ b/LibLouis.NET.Test/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using Xunit;
+
+// liblouis keeps global state (compiled table cache, log callback, data path) and is explicitly
+// not thread safe - LibLouis serialises its own calls behind a lock for exactly that reason.
+// xunit parallelises across test classes by default, which lets unsynchronised native calls race
+// and produce spurious "could not be compiled" failures. Run the whole assembly serially.
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
diff --git a/LibLouis.NET.Test/BrailleSpec.cs b/LibLouis.NET.Test/BrailleSpec.cs
new file mode 100644
index 0000000..a3e944e
--- /dev/null
+++ b/LibLouis.NET.Test/BrailleSpec.cs
@@ -0,0 +1,398 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+using YamlDotNet.Core;
+using YamlDotNet.Core.Events;
+
+namespace LibLouis.NET.Test;
+
+///
+/// One translation case from an upstream braille spec.
+///
+public sealed record BrailleSpecCase(
+ string SpecFile,
+ int Line,
+ string TableQuery,
+ string? AssertMatch,
+ string DisplayTable,
+ string Input,
+ string Expected,
+ TestDirection Direction,
+ bool ExpectedToFail)
+{
+ public override string ToString() =>
+ $"{SpecFile}:{Line} {Direction} {Describe(Input)} -> {Describe(Expected)}";
+
+ // Braille output is mostly U+28xx, which is unreadable in a test runner's output, so show the
+ // code points for anything outside printable ASCII.
+ private static string Describe(string value) =>
+ value.All(c => c is >= ' ' and <= '~')
+ ? $"\"{value}\""
+ : string.Concat(value.Select(c => c is >= ' ' and <= '~' ? c.ToString() : $"\\u{(int)c:X4}"));
+}
+
+public enum TestDirection
+{
+ Forward,
+ Backward,
+}
+
+///
+/// How a spec entry's two values are used. The distinction matters: the backward leg of
+/// bothDirections swaps input and expected (lou_checkyaml.c:900-902), while an explicit
+/// backward testmode does not (lou_checkyaml.c:892-895).
+///
+public enum TestMode
+{
+ Forward,
+ Backward,
+ BothDirections,
+}
+
+///
+/// Reads liblouis braille spec files.
+///
+///
+/// These files are not YAML mappings and cannot be deserialised. A single document repeats
+/// table, flags and tests at the same level, which duplicate key handling
+/// would collapse or reject. lou_checkyaml treats the file as an event stream where each key
+/// mutates parser state, and tests executes against whatever is current
+/// (tools/lou_checkyaml.c:1087-1139), so this reader does the same over YamlDotNet's IParser.
+///
+/// Consecutive table keys accumulate rather than replace: the following tests block
+/// runs once per accumulated table. flags persists until the next flags.
+///
+/// Only the constructs the Danish specs actually use are supported. Anything else throws rather
+/// than being skipped, so a spec using a feature this reader does not model fails loudly instead
+/// of silently testing less than it appears to.
+///
+public static class BrailleSpecReader
+{
+ public static IReadOnlyList Read(string path)
+ {
+ string specFile = Path.GetFileName(path);
+ var cases = new List();
+
+ using var reader = new StreamReader(path);
+ var parser = new Parser(reader);
+
+ parser.Consume();
+ parser.Consume();
+ parser.Consume();
+
+ string displayTable = string.Empty;
+ var tables = new List<(string Query, string? AssertMatch)>();
+ TestMode mode = TestMode.Forward;
+
+ // Consecutive table keys accumulate, but the first one after a tests block starts a fresh
+ // set rather than adding to the one just used.
+ bool tablesUsed = false;
+
+ while (parser.Current is not MappingEnd)
+ {
+ string key = parser.Consume().Value;
+
+ switch (key)
+ {
+ case "display":
+ displayTable = ReadTableValue(parser).Query;
+ break;
+
+ case "table":
+ if (tablesUsed)
+ {
+ tables.Clear();
+ tablesUsed = false;
+ }
+
+ tables.Add(ReadTableValue(parser));
+ break;
+
+ case "flags":
+ mode = ReadFlags(parser);
+ break;
+
+ case "tests":
+ ReadTests(parser, specFile, tables, displayTable, mode, cases);
+ tablesUsed = true;
+ break;
+
+ default:
+ throw new NotSupportedException(
+ $"{specFile}: unsupported top level key '{key}'. This reader models only the " +
+ "constructs the Danish specs use; see lou_checkyaml.c for the full format.");
+ }
+
+ }
+
+ return cases;
+ }
+
+ ///
+ /// A table value is either a file name or a query mapping. Queries are passed to lou_findTable
+ /// as "key:value key:value"; __assert-match is a harness directive, not part of the query.
+ ///
+ private static (string Query, string? AssertMatch) ReadTableValue(IParser parser)
+ {
+ if (parser.Current is Scalar scalar)
+ {
+ parser.MoveNext();
+ return (scalar.Value, null);
+ }
+
+ parser.Consume();
+
+ var terms = new List();
+ string? assertMatch = null;
+
+ while (parser.Current is not MappingEnd)
+ {
+ string key = parser.Consume().Value;
+ string value = parser.Consume().Value;
+
+ if (key == "__assert-match")
+ {
+ assertMatch = value;
+ }
+ else
+ {
+ terms.Add($"{key}:{value}");
+ }
+ }
+
+ parser.Consume();
+
+ return (string.Join(' ', terms), assertMatch);
+ }
+
+ private static TestMode ReadFlags(IParser parser)
+ {
+ parser.Consume();
+
+ TestMode mode = TestMode.Forward;
+
+ while (parser.Current is not MappingEnd)
+ {
+ string key = parser.Consume().Value;
+ string value = parser.Consume().Value;
+
+ if (key != "testmode")
+ {
+ throw new NotSupportedException($"unsupported flag '{key}'");
+ }
+
+ mode = ParseTestMode(value);
+ }
+
+ parser.Consume();
+
+ return mode;
+ }
+
+ private static TestMode ParseTestMode(string value) => value switch
+ {
+ "forward" => TestMode.Forward,
+ "backward" => TestMode.Backward,
+ "bothDirections" => TestMode.BothDirections,
+ _ => throw new NotSupportedException($"unsupported testmode '{value}'"),
+ };
+
+ private static void ReadTests(
+ IParser parser,
+ string specFile,
+ List<(string Query, string? AssertMatch)> tables,
+ string displayTable,
+ TestMode mode,
+ List cases)
+ {
+ parser.Consume();
+
+ while (parser.Current is not SequenceEnd)
+ {
+ SequenceStart entryStart = parser.Consume();
+ int line = (int)entryStart.Start.Line;
+
+ string input = Unescape(parser.Consume().Value);
+ string expected = Unescape(parser.Consume().Value);
+
+ var xfail = XFail.None;
+ TestMode entryMode = mode;
+ bool skip = false;
+
+ if (parser.Current is MappingStart)
+ {
+ (xfail, entryMode, skip) = ReadTestOptions(parser, mode);
+ }
+
+ parser.Consume();
+
+ if (skip)
+ {
+ continue;
+ }
+
+ foreach ((string query, string? assertMatch) in tables)
+ {
+ // Forward compares translate(input) with expected. An explicit backward testmode
+ // means the entry is already written braille-first, so it is not swapped. The
+ // backward leg of bothDirections is: the expected braille is the input, and the
+ // original text is what back translation should produce.
+ if (entryMode is TestMode.Forward or TestMode.BothDirections)
+ {
+ cases.Add(new BrailleSpecCase(
+ specFile, line, query, assertMatch, displayTable,
+ input, expected, TestDirection.Forward, xfail.HasFlag(XFail.Forward)));
+ }
+
+ if (entryMode == TestMode.Backward)
+ {
+ cases.Add(new BrailleSpecCase(
+ specFile, line, query, assertMatch, displayTable,
+ input, expected, TestDirection.Backward, xfail.HasFlag(XFail.Backward)));
+ }
+ else if (entryMode == TestMode.BothDirections)
+ {
+ cases.Add(new BrailleSpecCase(
+ specFile, line, query, assertMatch, displayTable,
+ expected, input, TestDirection.Backward, xfail.HasFlag(XFail.Backward)));
+ }
+ }
+ }
+
+ parser.Consume();
+ }
+
+ [Flags]
+ private enum XFail
+ {
+ None = 0,
+ Forward = 1,
+ Backward = 2,
+ Both = Forward | Backward,
+ }
+
+ private static (XFail XFail, TestMode Mode, bool Skip) ReadTestOptions(
+ IParser parser, TestMode mode)
+ {
+ parser.Consume();
+
+ var xfail = XFail.None;
+ bool skip = false;
+
+ while (parser.Current is not MappingEnd)
+ {
+ string key = parser.Consume().Value;
+
+ switch (key)
+ {
+ case "xfail":
+ xfail = ReadXFail(parser);
+ break;
+
+ case "testmode":
+ mode = ParseTestMode(parser.Consume().Value);
+ break;
+
+ // Emphasis is applied through typeform, which this prototype does not drive yet.
+ // Skip the value so the rest of the file still parses, and drop the case: silently
+ // running it without the typeform would compare against the wrong expectation.
+ case "typeform":
+ parser.SkipThisAndNestedEvents();
+ skip = true;
+ break;
+
+ default:
+ throw new NotSupportedException($"unsupported test option '{key}'");
+ }
+ }
+
+ parser.Consume();
+
+ return (xfail, mode, skip);
+ }
+
+ ///
+ /// xfail is either a scalar, where only "false" and "off" are falsy
+ /// (tools/lou_checkyaml.c:379-389), or a mapping naming the failing directions.
+ ///
+ private static XFail ReadXFail(IParser parser)
+ {
+ if (parser.Current is Scalar scalar)
+ {
+ parser.MoveNext();
+ return scalar.Value is "false" or "off" ? XFail.None : XFail.Both;
+ }
+
+ parser.Consume();
+
+ var xfail = XFail.None;
+
+ while (parser.Current is not MappingEnd)
+ {
+ string key = parser.Consume().Value;
+ string value = parser.Consume().Value;
+ bool set = value is not ("false" or "off");
+
+ if (set)
+ {
+ xfail |= key switch
+ {
+ "forward" => XFail.Forward,
+ "backward" => XFail.Backward,
+ _ => throw new NotSupportedException($"unsupported xfail direction '{key}'"),
+ };
+ }
+ }
+
+ parser.Consume();
+
+ return xfail;
+ }
+
+ ///
+ /// The specs use single quoted scalars, where YAML performs no escape processing at all, and
+ /// rely on liblouis to interpret the escapes itself. Only the forms the Danish specs actually
+ /// use are handled: \xNNNN and \uNNNN code points, and \\ for a literal backslash.
+ /// Without the backslash case, 'at\\bliver' parses as two backslashes and translates to two
+ /// cells where upstream expects one.
+ ///
+ private static string Unescape(string value)
+ {
+ if (!value.Contains('\\', StringComparison.Ordinal))
+ {
+ return value;
+ }
+
+ var builder = new StringBuilder(value.Length);
+
+ for (int i = 0; i < value.Length; i++)
+ {
+ if (value[i] == '\\' && i + 1 < value.Length)
+ {
+ if (value[i + 1] is 'x' or 'y' or 'u' && i + 5 < value.Length &&
+ ushort.TryParse(
+ value.AsSpan(i + 2, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort code))
+ {
+ builder.Append((char)code);
+ i += 5;
+ continue;
+ }
+
+ if (value[i + 1] is '\\' or '"')
+ {
+ builder.Append(value[i + 1]);
+ i++;
+ continue;
+ }
+ }
+
+ builder.Append(value[i]);
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/LibLouis.NET.Test/BrailleSpecTests.cs b/LibLouis.NET.Test/BrailleSpecTests.cs
new file mode 100644
index 0000000..3a8dc95
--- /dev/null
+++ b/LibLouis.NET.Test/BrailleSpecTests.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// Runs the upstream braille specs for Danish through the managed wrapper.
+///
+///
+/// These are liblouis's own expectations, so they check the wrapper end to end against thousands of
+/// cases nobody here had to invent: not just that a translation succeeds, but that it produces the
+/// characters upstream says it should, in both directions.
+///
+/// The specs live in braille-specs/ and are copied verbatim from
+/// upstream/liblouis-<version>/tests/braille-specs/. Re-copy them when the upstream version is
+/// bumped; the diff is the set of expectations that changed.
+///
+/// One test per spec file rather than per case. Ten thousand xunit cases makes discovery slow and
+/// buries a real regression in an unreadable log; a single failure listing every mismatch is more
+/// use than ten thousand separate red entries.
+///
+public class BrailleSpecTests
+{
+ private static readonly string SpecDirectory =
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "braille-specs");
+
+ // tables/ is the upstream set. Nota's own tables live in nota-tables/ and no longer shadow it,
+ // so the specs can be checked against the tables they were actually written for.
+ private static readonly string TableDirectory =
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tables");
+
+ public static TheoryData SpecFiles()
+ {
+ var data = new TheoryData();
+
+ foreach (string path in Directory.EnumerateFiles(SpecDirectory, "*.yaml").OrderBy(f => f, StringComparer.Ordinal))
+ {
+ data.Add(Path.GetFileName(path));
+ }
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(SpecFiles))]
+ public void MatchesUpstreamExpectations(string specFile)
+ {
+ IReadOnlyList cases = BrailleSpecReader.Read(Path.Combine(SpecDirectory, specFile));
+
+ IndexUpstreamTables();
+
+ var mismatches = new List();
+ var unexpectedPasses = new List();
+ int checkedCount = 0;
+
+ foreach (BrailleSpecCase testCase in cases)
+ {
+
+ string table = ResolveTable(testCase, TableCache.Value);
+ string? actual = Run(testCase, table);
+ bool matched = actual == testCase.Expected;
+ checkedCount++;
+
+ if (testCase.ExpectedToFail)
+ {
+ // Upstream reports an unexpected pass as a warning rather than an error: the case
+ // is known-broken, and it passing usually means the expectation moved.
+ if (matched)
+ {
+ unexpectedPasses.Add(testCase.ToString());
+ }
+ }
+ else if (!matched)
+ {
+ mismatches.Add($"{testCase}\n actual: {Describe(actual)}");
+ }
+ }
+
+
+ Assert.True(
+ mismatches.Count == 0,
+ $"{specFile}: {mismatches.Count} of {checkedCount} cases did not match upstream " +
+ $"({unexpectedPasses.Count} xfail cases passed unexpectedly).\n " +
+ string.Join("\n ", mismatches.Take(25)) +
+ (mismatches.Count > 25 ? $"\n ... and {mismatches.Count - 25} more" : string.Empty));
+ }
+
+ private static string? Run(BrailleSpecCase testCase, string table)
+ {
+ string[] tables = [ResolveDisplayTable(testCase.DisplayTable), table];
+ int outputLength = Math.Max(testCase.Input.Length, testCase.Expected.Length) * 4;
+
+ try
+ {
+ return testCase.Direction == TestDirection.Forward
+ ? LibLouis.Instance.Translate(tables, testCase.Input, outputLength, null, null, TranslationMode.Regular)
+ : LibLouis.Instance.BackTranslate(tables, testCase.Input, outputLength, null, null, TranslationMode.Regular);
+ }
+ catch (LibLouisException ex)
+ {
+ // A translation that fails outright is a mismatch, not an error: upstream marks some of
+ // these xfail, and letting it throw would stop the whole file at the first one. The
+ // message is carried into the comparison so a failure says why, rather than only that
+ // nothing came back.
+ return $"";
+ }
+ }
+
+ // Every query from every spec is resolved once, up front, before any translation runs.
+ // lou_findTable's return value is freed by the marshaller with the wrong allocator (P/Invoke
+ // audit item 3), so interleaving these calls with translations corrupts the native heap.
+ private static readonly Lazy> TableCache = new(() =>
+ {
+ LibLouis.Instance.IndexTables(
+ Directory.EnumerateFiles(TableDirectory)
+ .Where(f => Path.GetExtension(f) is ".ctb" or ".utb" or ".uti" or ".dis" or ".cti" or ".dic"));
+
+ var resolved = new Dictionary(StringComparer.Ordinal);
+
+ foreach (string query in Directory.EnumerateFiles(SpecDirectory, "*.yaml")
+ .SelectMany(BrailleSpecReader.Read)
+ .Select(c => c.TableQuery)
+ .Distinct(StringComparer.Ordinal))
+ {
+ resolved[query] = LibLouis.Instance.FindTable(query) ?? string.Empty;
+ }
+
+ return resolved;
+ });
+
+ private static void IndexUpstreamTables() => _ = TableCache.Value;
+
+ ///
+ /// Resolving a table query is what lou_findTable does, and the specs assert which file a query
+ /// should select, so this covers table resolution as well as translation.
+ ///
+ private static string ResolveTable(BrailleSpecCase testCase, Dictionary cache)
+ {
+ if (!cache.TryGetValue(testCase.TableQuery, out string? resolved))
+ {
+ resolved = LibLouis.Instance.FindTable(testCase.TableQuery) ?? string.Empty;
+ cache[testCase.TableQuery] = resolved;
+ }
+
+ Assert.False(
+ string.IsNullOrEmpty(resolved),
+ $"No table matched the query '{testCase.TableQuery}' from {testCase.SpecFile}:{testCase.Line}");
+
+ if (testCase.AssertMatch is not null)
+ {
+ Assert.True(
+ string.Equals(Path.GetFileName(resolved), testCase.AssertMatch, StringComparison.Ordinal),
+ $"Query '{testCase.TableQuery}' resolved to {Path.GetFileName(resolved)}, " +
+ $"but {testCase.SpecFile}:{testCase.Line} asserts {testCase.AssertMatch}");
+ }
+
+ return resolved;
+ }
+
+ ///
+ /// A spec's display table is usually a file name, but it can also be an inline table written as
+ /// a YAML block scalar, for instance to include the standard one and then override a character.
+ /// liblouis only takes paths, so the inline form is written out next to the tables, where the
+ /// includes inside it resolve.
+ ///
+ private static string ResolveDisplayTable(string display)
+ {
+ // A file name never contains a newline, so this distinguishes the two forms.
+ if (!display.Contains('\n', StringComparison.Ordinal))
+ {
+ return Path.Combine(TableDirectory, display);
+ }
+
+ string name = $"inline-{Convert.ToHexString(System.Security.Cryptography.MD5.HashData(System.Text.Encoding.UTF8.GetBytes(display)))[..8]}.dis";
+ string path = Path.Combine(TableDirectory, name);
+
+ if (!File.Exists(path))
+ {
+ File.WriteAllText(path, display);
+ }
+
+ return path;
+ }
+
+ private static string Describe(string? value) =>
+ value is null
+ ? ""
+ : string.Concat(value.Select(c => c is >= ' ' and <= '~' ? c.ToString() : $"\\u{(int)c:X4}"));
+}
diff --git a/LibLouis.NET.Test/CollectingLogger.cs b/LibLouis.NET.Test/CollectingLogger.cs
new file mode 100644
index 0000000..9049579
--- /dev/null
+++ b/LibLouis.NET.Test/CollectingLogger.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+
+using Microsoft.Extensions.Logging;
+
+namespace LibLouis.NET.Test;
+
+///
+/// Captures everything liblouis logs, so tests can assert on what the native side reported.
+///
+internal sealed class CollectingLogger : ILogger
+{
+ public List Messages { get; } = [];
+
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ // LogLevel is qualified throughout: in this namespace the unqualified name binds to
+ // LibLouis.NET.LogLevel, the native enum, not the Microsoft.Extensions.Logging one.
+ public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) => true;
+
+ public void Log(
+ Microsoft.Extensions.Logging.LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter)
+ {
+ Messages.Add(formatter(state, exception));
+ }
+}
diff --git a/LibLouis.NET.Test/HyphenateTests.cs b/LibLouis.NET.Test/HyphenateTests.cs
new file mode 100644
index 0000000..2da22eb
--- /dev/null
+++ b/LibLouis.NET.Test/HyphenateTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// lou_hyphenate takes a caller-allocated char *hyphens buffer and writes inlen + 1 bytes
+/// into it: '0' or '1' per character, plus a terminator (lou_translateString.c:4080).
+///
+public class HyphenateTests
+{
+ private const string Word = "bogstaver";
+
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g26.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ [Fact]
+ public void Hyphenate_ReturnsOneHyphenationFlagPerCharacter()
+ {
+ string hyphens = LibLouis.Instance.Hyphenate(TablePaths(), Word, TranslationMode.Regular);
+
+ Assert.Equal(Word.Length, hyphens.Length);
+ Assert.Matches("^[012]+$", hyphens);
+
+ // "bog-sta-ver": the table has to find at least one break, otherwise this test is not
+ // exercising hyphenation at all.
+ Assert.Contains('1', hyphens);
+ }
+
+ ///
+ /// The result must describe the word that was passed in, not a NUL terminator the wrapper
+ /// added. Unlike lou_translateString, lou_hyphenate does not clamp inlen at the first NUL:
+ /// it memcpy's exactly inlen characters, so an inflated inlen hyphenates the terminator too.
+ ///
+ [Fact]
+ public void Hyphenate_DoesNotIncludeTheNulTerminator()
+ {
+ foreach (string word in new[] { "a", "bo", "bogstaver", "hyphenation" })
+ {
+ string hyphens = LibLouis.Instance.Hyphenate(TablePaths(), word, TranslationMode.Regular);
+
+ Assert.Equal(word.Length, hyphens.Length);
+ }
+ }
+
+ ///
+ /// inlen is a widechar count. On a UCS-4 build a non-BMP character is one widechar but two
+ /// chars, so passing string.Length claims the buffer is longer than it is - and lou_hyphenate
+ /// memcpy's exactly inlen widechars out of it, with no terminator to stop at.
+ ///
+ [Theory]
+ [InlineData("bogstaver\U0001D11E")] // one flag too many
+ [InlineData("bogstaver\U0001D11E\U0001D11E")] // and reads past the input buffer
+ public void Hyphenate_ReturnsOneFlagPerWidecharNotPerCodeUnit(string word)
+ {
+ int expected = SafeNativeMethods.lou_charSize() == 4
+ ? word.EnumerateRunes().Count()
+ : word.Length;
+
+ string hyphens = LibLouis.Instance.Hyphenate(TablePaths(), word, TranslationMode.Regular);
+
+ Assert.Equal(expected, hyphens.Length);
+ }
+}
diff --git a/LibLouis.NET.Test/IndexTablesTests.cs b/LibLouis.NET.Test/IndexTablesTests.cs
new file mode 100644
index 0000000..7bfac4e
--- /dev/null
+++ b/LibLouis.NET.Test/IndexTablesTests.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+using Microsoft.Extensions.Logging;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// lou_indexTables walks its argument until it hits a NULL pointer
+/// (for (table = tables; *table; table++), metadata.c:905). A managed string[] marshals to
+/// exactly Length pointers with no terminator, so liblouis reads past the end of the array.
+///
+public class IndexTablesTests
+{
+ private static readonly string[] Tables = ["da-dk-g26.ctb", "da-dk-g16-markers.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ ///
+ /// liblouis logs one "Analyzing table <name>" line per array entry it walks, so the number
+ /// of those lines is a direct measure of how far it read.
+ ///
+ ///
+ /// Without the terminator this does not fail, it hangs: liblouis reads the managed memory
+ /// following the array as a char* and _lou_logMessage formats it with %s until it runs out of
+ /// readable memory. A regression here shows up as a test run that never finishes.
+ ///
+ [Fact]
+ public void IndexTables_DoesNotReadPastTheEndOfTheArray()
+ {
+ string[] paths = TablePaths();
+
+ CollectingLogger logger = new();
+ ILogger previous = LibLouis.Instance.Logger;
+ LibLouis.Instance.Logger = logger;
+
+ try
+ {
+ LibLouis.Instance.IndexTables(paths);
+ }
+ finally
+ {
+ LibLouis.Instance.Logger = previous;
+ }
+
+ List analyzed = [.. logger.Messages
+ .Where(m => m.StartsWith("Analyzing table ", StringComparison.Ordinal))
+ .Select(m => m["Analyzing table ".Length..])];
+
+ Assert.Equal(paths, analyzed);
+ }
+
+}
diff --git a/LibLouis.NET.Test/InputLengthTests.cs b/LibLouis.NET.Test/InputLengthTests.cs
new file mode 100644
index 0000000..605a37e
--- /dev/null
+++ b/LibLouis.NET.Test/InputLengthTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Linq;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// inlen is a widechar count that excludes the NUL terminator, matching the header and what
+/// upstream callers pass. These tests pin down the two properties that depend on it:
+///
+/// * The terminator is not translated as if it were text. The buffer stays NUL terminated
+/// (PrepareUCSInputBuffer's job) and lou_translateString clamps at the first NUL
+/// (while (k < *inlen && inbufx[k]) k++;, lou_translateString.c:1191), so an
+/// embedded NUL still ends the input.
+/// * Nothing is written past the position arrays the argument checks demand. liblouis
+/// overwrites *inlen with the number of characters actually consumed
+/// (lou_translateString.c:1354) before computing outputPos.
+///
+/// The wrapper previously passed input.Length + 1 here. That was safe - the clamp at :1191 and
+/// the overwrite at :1354 between them made the extra count unreachable - but it left
+/// correctness resting on two undocumented internals instead of the documented contract.
+///
+public class InputLengthTests
+{
+ private const string Input = "Første linje. Anden linje, med kursiveret tekst. Tredje linje.";
+
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g26.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ ///
+ /// Translate() only requires outputPosition to hold input.Length entries, so liblouis must
+ /// not write beyond that. The array is deliberately oversized and sentinel filled, so an
+ /// out-of-bounds write would be observable here instead of corrupting the heap.
+ ///
+ [Fact]
+ public void Translate_DoesNotWriteOutputPositionsPastInputLength()
+ {
+ // Not -1: liblouis pre-fills outputPos with -1 for the characters it owns, so -1 could
+ // not tell an untouched entry apart from one liblouis had written.
+ const int sentinel = int.MinValue;
+ const int slack = 8;
+
+ int outputLength = Input.Length * 4;
+
+ int[] outputPosition = new int[Input.Length + slack];
+ Array.Fill(outputPosition, sentinel);
+
+ LibLouis.Instance.Translate(
+ TablePaths(),
+ Input,
+ outputLength,
+ null,
+ null,
+ outputPosition,
+ new int[outputLength],
+ 0,
+ TranslationMode.Regular);
+
+ int firstUntouched = Array.FindIndex(outputPosition, p => p == sentinel);
+
+ Assert.Equal(Input.Length, firstUntouched);
+ }
+
+ ///
+ /// The NUL terminator is not translated as if it were input text.
+ ///
+ [Fact]
+ public void Translate_DoesNotTranslateTheNulTerminator()
+ {
+ const string input = "abc";
+
+ string translated = LibLouis.Instance.Translate(
+ TablePaths(), input, input.Length * 4, null, null, TranslationMode.Regular);
+
+ Assert.DoesNotContain('\0', translated);
+ Assert.Equal(input, translated);
+ }
+}
diff --git a/LibLouis.NET.Test/LibLouis.NET.Test.csproj b/LibLouis.NET.Test/LibLouis.NET.Test.csproj
index 8dee374..1e8ca27 100644
--- a/LibLouis.NET.Test/LibLouis.NET.Test.csproj
+++ b/LibLouis.NET.Test/LibLouis.NET.Test.csproj
@@ -5,6 +5,8 @@
enable
false
true
+
+ true
@@ -15,6 +17,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -27,7 +30,40 @@
-
+
+
+ PreserveNewest
+
+
+
+
+ PreserveNewest
+
+
+
+
PreserveNewest
diff --git a/LibLouis.NET.Test/LogCallbackTests.cs b/LibLouis.NET.Test/LogCallbackTests.cs
new file mode 100644
index 0000000..507c7bc
--- /dev/null
+++ b/LibLouis.NET.Test/LogCallbackTests.cs
@@ -0,0 +1,70 @@
+using System;
+
+using Microsoft.Extensions.Logging;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// liblouis keeps the function pointer it is handed by lou_registerLogCallback and calls it for
+/// the rest of the process's life. The managed delegate behind that pointer therefore has to stay
+/// alive for just as long: the marshalling stub only keeps it alive for the duration of the
+/// registration call itself.
+///
+public class LogCallbackTests
+{
+ ///
+ /// Forces collections between registering the callback and provoking a native log message.
+ /// If nothing roots the delegate, the pointer liblouis holds is dangling by then.
+ ///
+ [Fact]
+ public void Logger_StillReceivesMessagesAfterGarbageCollection()
+ {
+ CollectingLogger logger = new();
+ LibLouis.Instance.Logger = logger;
+
+ for (int i = 0; i < 3; i++)
+ {
+ GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true);
+ GC.WaitForPendingFinalizers();
+ }
+
+ // Any failing call makes liblouis log; a table that cannot be compiled is the simplest.
+ Assert.Throws(
+ () => LibLouis.Instance.Translate(
+ ["no-such-table-at-all.ctb"], "x", 8, null, null, TranslationMode.Regular));
+
+ Assert.NotEmpty(logger.Messages);
+ }
+
+ ///
+ /// The callback runs on a native stack. An exception thrown out of it cannot be handled by
+ /// liblouis and tears the process down, so an unmapped level must not throw.
+ ///
+ [Fact]
+ public void LogCallback_SurvivesALevelItDoesNotKnow()
+ {
+ CollectingLogger logger = new();
+ LibLouis.Instance.Logger = logger;
+
+ // 12345 is not one of the logLevels values liblouis defines.
+ NativeMethods.LoggingCallback callback = GetRegisteredCallback();
+
+ callback((LogLevel)12345, "message at an unknown level");
+ }
+
+ ///
+ /// Reaches the delegate the wrapper registered, so the test calls exactly what liblouis calls.
+ ///
+ private static NativeMethods.LoggingCallback GetRegisteredCallback()
+ {
+ object? field = typeof(LibLouis)
+ .GetField("_logCallback", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.GetValue(LibLouis.Instance);
+
+ Assert.NotNull(field);
+
+ return (NativeMethods.LoggingCallback)field;
+ }
+}
diff --git a/LibLouis.NET.Test/NativeLockTests.cs b/LibLouis.NET.Test/NativeLockTests.cs
new file mode 100644
index 0000000..48ce8b8
--- /dev/null
+++ b/LibLouis.NET.Test/NativeLockTests.cs
@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// liblouis is not thread safe and its state is process-global, so every native call in the
+/// assembly has to serialise on one lock - including the ones that do not obviously touch shared
+/// state.
+///
+///
+/// Holding the lock is not directly observable: lou_version returns a static string and the
+/// Logging setters are single pointer-sized writes, so an unsynchronised build does not reliably
+/// misbehave. These tests therefore guard the two things that are observable - that no native
+/// entry point was left outside the lock, and that adding the lock did not introduce a deadlock
+/// or change behaviour.
+///
+public class NativeLockTests
+{
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g26.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ [Fact]
+ public void VersionIsReported()
+ {
+ Assert.False(string.IsNullOrWhiteSpace(LibLouis.Instance.Version));
+ }
+
+ [Fact]
+ public void LogLevelRoundTrips()
+ {
+ LogLevel previous = Logging.LogLevel;
+
+ try
+ {
+ Logging.LogLevel = LogLevel.Warning;
+ Assert.Equal(LogLevel.Warning, Logging.LogLevel);
+ }
+ finally
+ {
+ Logging.LogLevel = previous;
+ }
+ }
+
+ ///
+ /// The lock is shared between LibLouis and the static Logging helper, and Monitor is
+ /// reentrant, so hammering all three from several threads must neither deadlock nor produce a
+ /// wrong translation.
+ ///
+ [Fact]
+ public async Task ConcurrentUseDoesNotDeadlockOrCorrupt()
+ {
+ const string input = "Første linje";
+ const string expected = "@fze linje";
+
+ LogLevel previous = Logging.LogLevel;
+
+ using CancellationTokenSource cts = new(TimeSpan.FromSeconds(5));
+
+ ConcurrentBag failures = [];
+
+ try
+ {
+ Task[] workers =
+ [
+ .. Enumerable.Range(0, 4).Select(_ => Task.Run(() =>
+ {
+ while (!cts.IsCancellationRequested)
+ {
+ string result = LibLouis.Instance.Translate(
+ TablePaths(), input, 64, null, null, TranslationMode.Regular);
+
+ if (result != expected)
+ {
+ failures.Add($"translation returned '{result}'");
+ return;
+ }
+ }
+ })),
+ Task.Run(() =>
+ {
+ while (!cts.IsCancellationRequested)
+ {
+ _ = LibLouis.Instance.Version;
+ Logging.LogLevel = LogLevel.Error;
+ }
+ }),
+ ];
+
+ Task all = Task.WhenAll(workers);
+
+ Assert.Same(
+ all,
+ await Task.WhenAny(all, Task.Delay(TimeSpan.FromSeconds(30))));
+
+ await all;
+ }
+ finally
+ {
+ Logging.LogLevel = previous;
+ }
+
+ Assert.Empty(failures);
+ }
+
+ ///
+ /// Catches a native call added later without the lock. Deliberately source-based: there is no
+ /// runtime signal for "this P/Invoke ran unsynchronised".
+ ///
+ ///
+ /// A call that genuinely does not need the lock has to say so, by carrying an "unlocked:"
+ /// comment giving the reason. That keeps the exemptions few and explains each one, instead of
+ /// letting the test quietly special-case whole methods.
+ ///
+ [Theory]
+ [InlineData("LibLouis.cs")]
+ [InlineData("Logging.cs")]
+ public void EveryNativeCallSiteIsLockedOrJustified(string fileName)
+ {
+ string[] lines = ReadLibrarySource(fileName).Split('\n');
+
+ int depth = 0;
+ int lockDepth = -1;
+ bool pendingLock = false;
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ string line = lines[i].Trim();
+
+ // The body starts at the brace on the next line, so record the depth once we are
+ // actually inside it rather than on the "lock (" line itself.
+ if (line.StartsWith("lock (", StringComparison.Ordinal))
+ {
+ pendingLock = true;
+ }
+
+ bool isNativeCall = line.Contains("NativeMethods.", StringComparison.Ordinal)
+ && !line.StartsWith("//", StringComparison.Ordinal)
+ && !line.StartsWith("///", StringComparison.Ordinal)
+ && !line.Contains("NativeMethods.LoggingCallback", StringComparison.Ordinal);
+
+ if (isNativeCall && lockDepth < 0)
+ {
+ bool justified = lines
+ .Take(i)
+ .Reverse()
+ .TakeWhile(l => l.Trim().StartsWith("//", StringComparison.Ordinal))
+ .Any(l => l.Contains("unlocked:", StringComparison.Ordinal));
+
+ Assert.True(justified, $"{fileName}: native call outside a lock: {line}");
+ }
+
+ int updated = depth + lines[i].Count(c => c == '{') - lines[i].Count(c => c == '}');
+
+ if (pendingLock && updated > depth)
+ {
+ lockDepth = updated;
+ pendingLock = false;
+ }
+
+ depth = updated;
+
+ if (lockDepth >= 0 && depth < lockDepth)
+ {
+ lockDepth = -1;
+ }
+ }
+ }
+
+ private static string ReadLibrarySource(string fileName)
+ {
+ DirectoryInfo? directory = new(AppContext.BaseDirectory);
+
+ while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "LibLouis.NET.sln")))
+ {
+ directory = directory.Parent;
+ }
+
+ Assert.NotNull(directory);
+
+ string path = Path.Combine(directory.FullName, "LibLouis.NET", fileName);
+
+ Assert.True(File.Exists(path), $"could not locate {path}");
+
+ return File.ReadAllText(path);
+ }
+}
diff --git a/LibLouis.NET.Test/NativeMethodsTests.cs b/LibLouis.NET.Test/NativeMethodsTests.cs
index a203486..0bb2032 100644
--- a/LibLouis.NET.Test/NativeMethodsTests.cs
+++ b/LibLouis.NET.Test/NativeMethodsTests.cs
@@ -23,7 +23,7 @@ public void SingleMode()
Array.Fill(modes, TypeForm.ForeignLanguage);
string resultString = LibLouis.Instance.Translate(
- tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tables", t)),
+ tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t)),
input,
outputLength,
modes,
@@ -48,7 +48,7 @@ public void NestedMode_EmphasisInForeignLanguage()
modes[6] = TypeForm.ForeignLanguage | TypeForm.Emphasis;
string resultString = LibLouis.Instance.Translate(
- tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tables", t)),
+ tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t)),
input,
outputLength,
modes,
@@ -89,7 +89,7 @@ public void NestedMode_ForeignLanguageInEmphasis()
Assert.Equal(inputLength, modes.Length);
string resultString = LibLouis.Instance.Translate(
- tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tables", t)),
+ tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t)),
input,
outputLength,
modes,
@@ -105,7 +105,7 @@ public void TestPositionResults()
const string input = "Første linje. Anden linje, med kursiveret tekst. Tredje linje.";
const string expected = "@fze linje. @anç linje, m kursi#rò ükz. @tàdje linje.";
- string[] tables = ["tables/da-dk-braillo.dis", "tables/da-dk-g26.ctb"];
+ string[] tables = ["nota-tables/da-dk-braillo.dis", "nota-tables/da-dk-g26.ctb"];
int outputLength = input.Length * 4;
int cursorPosition = 0;
@@ -116,7 +116,10 @@ public void TestPositionResults()
Assert.Equal(expected, translated.Output);
- Assert.Equal(inputPosition, translated.InputPosition);
+ // The returned arrays are sized to the strings they index rather than to the scratch
+ // buffers passed in, so InputPosition covers the output and no slicing is needed to use
+ // it. For this BMP input the values are unchanged from what liblouis wrote.
+ Assert.Equal(inputPosition[..translated.Output.Length], translated.InputPosition);
Assert.Equal(outputPosition, translated.OutputPosition);
Assert.Equal('A', input[inputPosition[12]]);
diff --git a/LibLouis.NET.Test/NonBmpTests.cs b/LibLouis.NET.Test/NonBmpTests.cs
new file mode 100644
index 0000000..18a5999
--- /dev/null
+++ b/LibLouis.NET.Test/NonBmpTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Linq;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// On a UCS-4 build a liblouis widechar holds a whole Unicode character, so a non-BMP character
+/// occupies one widechar but two chars of a .NET string. Passing string.Length as a widechar
+/// count therefore overstates the length of the buffer.
+///
+/// lou_translateString survives that, because it clamps at the NUL terminator. lou_dotsToChar and
+/// lou_charToDots do not clamp: they read and write exactly the count they are given
+/// (lou_translateString.c:4142), so a count in the wrong unit reads past the input buffer.
+///
+public class NonBmpTests
+{
+ /// U+1D11E MUSICAL SYMBOL G CLEF - one character, two UTF-16 code units.
+ private const string NonBmp = "\U0001D11E\U0001D11E";
+
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g26.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ ///
+ /// How many widechars liblouis sees for : whole characters on a UCS-4
+ /// build, UTF-16 code units on a UCS-2 one.
+ ///
+ private static int ExpectedCells(string value)
+ {
+ return SafeNativeMethods.lou_charSize() == 4
+ ? value.EnumerateRunes().Count()
+ : value.Length;
+ }
+
+ [Fact]
+ public void CharactersToDots_ProducesOneCellPerWidecharNotPerCodeUnit()
+ {
+ string dots = LibLouis.Instance.CharactersToDots(TablePaths(), NonBmp);
+
+ Assert.Equal(ExpectedCells(NonBmp), dots.Length);
+ }
+
+ [Fact]
+ public void DotsToCharacters_ProducesOneCharacterPerCell()
+ {
+ string dots = LibLouis.Instance.CharactersToDots(TablePaths(), NonBmp);
+
+ string roundTripped = LibLouis.Instance.DotsToCharacters(TablePaths(), dots);
+
+ Assert.Equal(dots.Length, roundTripped.Length);
+ }
+
+ ///
+ /// BMP text must keep behaving exactly as before: there string.Length and the widechar count
+ /// agree, so this guards the common case against the fix.
+ ///
+ [Fact]
+ public void CharactersToDots_IsUnchangedForBmpText()
+ {
+ const string input = "abc";
+
+ string dots = LibLouis.Instance.CharactersToDots(TablePaths(), input);
+
+ Assert.Equal(input.Length, dots.Length);
+ }
+}
diff --git a/LibLouis.NET.Test/NotaTablesTests.cs b/LibLouis.NET.Test/NotaTablesTests.cs
new file mode 100644
index 0000000..dc9d247
--- /dev/null
+++ b/LibLouis.NET.Test/NotaTablesTests.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// Guards the nota-tables/ directory, which the tests use in place of the upstream set.
+///
+public class NotaTablesTests
+{
+ private static readonly string NotaTableDirectory =
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables");
+
+ private static readonly string UpstreamTableDirectory =
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tables");
+
+ ///
+ /// liblouis resolves an include relative to the directory of the table doing the including, so
+ /// every table nota-tables/ pulls in has to be there too. Most are Nota's own; the handful of
+ /// general upstream tables they need are copied in by an explicit list in the csproj, and an
+ /// explicit list is exactly the kind of thing that goes stale the moment a table gains an
+ /// include.
+ ///
+ [Fact]
+ public void NotaTablesAreSelfContained()
+ {
+ var present = TableFilesIn(NotaTableDirectory).ToHashSet(StringComparer.Ordinal);
+ var missing = new SortedSet(StringComparer.Ordinal);
+
+ foreach (string file in present)
+ {
+ foreach (string included in IncludesOf(Path.Combine(NotaTableDirectory, file)))
+ {
+ if (!present.Contains(included))
+ {
+ missing.Add($"{included} (included by {file})");
+ }
+ }
+ }
+
+ Assert.True(
+ missing.Count == 0,
+ "nota-tables/ is missing tables it includes, so those cannot be compiled from that " +
+ "directory alone. Add them to the upstream copy list in the csproj:\n " +
+ string.Join("\n ", missing));
+ }
+
+ ///
+ /// The separate directory exists so that nothing of Nota's shadows the upstream set. Sharing a
+ /// file name across the two directories is expected and fine - twenty two of Nota's tables are
+ /// forks of an upstream table of the same name. What must never happen is Nota's tables being
+ /// copied into tables/ as well, which is what the old single directory did.
+ ///
+ /// The tables that exist nowhere upstream are the reliable canary: if one of them turns up in
+ /// tables/, the copy is misconfigured, whatever the file names say.
+ ///
+ [Fact]
+ public void NotaTablesDoNotLeakIntoTheUpstreamDirectory()
+ {
+ Assert.True(Directory.Exists(UpstreamTableDirectory), "the upstream tables were not staged");
+
+ string[] notaOnly =
+ [
+ "da-dk-g16-markers.ctb",
+ "da-dk-g16_1993-markers.ctb",
+ "da-dk-braillo.dis",
+ "da-dk-g16-crossword.ctb",
+ "da-dk-g26l.ctb",
+ "da-dk-g26l-lit.ctb",
+ "da-dk-g28l.ctb",
+ "da-dk-g2_1993.dic",
+ ];
+
+ var upstream = TableFilesIn(UpstreamTableDirectory).ToHashSet(StringComparer.Ordinal);
+ var leaked = notaOnly.Where(upstream.Contains).OrderBy(f => f, StringComparer.Ordinal).ToList();
+
+ Assert.True(
+ leaked.Count == 0,
+ "tables/ should hold only the upstream set, but these tables of Nota's are in it, so " +
+ "the two are being copied to the same place again:\n " + string.Join("\n ", leaked));
+ }
+
+ private static IEnumerable TableFilesIn(string directory) =>
+ Directory.EnumerateFiles(directory)
+ .Select(Path.GetFileName)
+ .Where(f => f is not null && !f.EndsWith(".md", StringComparison.OrdinalIgnoreCase))!;
+
+ private static IEnumerable IncludesOf(string path) =>
+ File.ReadLines(path)
+ .Select(line => Regex.Match(line, @"^\s*include\s+(\S+)"))
+ .Where(m => m.Success)
+ .Select(m => m.Groups[1].Value);
+}
diff --git a/LibLouis.NET.Test/OutputDotsTests.cs b/LibLouis.NET.Test/OutputDotsTests.cs
new file mode 100644
index 0000000..6ce3373
--- /dev/null
+++ b/LibLouis.NET.Test/OutputDotsTests.cs
@@ -0,0 +1,101 @@
+using System;
+using System.IO;
+using System.Linq;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// On a successful forward translation liblouis reports, per output cell, whether the cell
+/// contains dot 7 or dot 8 (lou_translateString.c:1330). It writes that into the typeform
+/// buffer - which is why the buffer has to be output-sized, and why the caller's input-sized
+/// array must not receive it. The wrapper surfaces the information on TranslatedString instead,
+/// so callers get it without the write-past-the-end hazard.
+///
+public class OutputDotsTests
+{
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g08.ctb"];
+
+ private static string[] EightDotTables() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ private static TranslatedString TranslateWithTypeForm(string input)
+ {
+ TypeForm[] typeform = new TypeForm[input.Length];
+
+ return LibLouis.Instance.Translate(
+ EightDotTables(),
+ input,
+ input.Length * 4,
+ typeform,
+ null,
+ new int[input.Length],
+ new int[input.Length * 4],
+ 0,
+ TranslationMode.Regular);
+ }
+
+ ///
+ /// Danish 8-dot braille marks a capital with dot 7 on the letter's own cell, so casing gives
+ /// a per-cell pattern we can predict: the flag must differ between the capital and the small
+ /// letters.
+ ///
+ [Fact]
+ public void ReportsDot7OnCapitalCells()
+ {
+ TranslatedString result = TranslateWithTypeForm("Abc");
+
+ Assert.NotNull(result.OutputDots78);
+ Assert.Equal(result.Output.Length, result.OutputDots78.Length);
+
+ Assert.True(result.OutputDots78[0], "capital A should carry dot 7 in an 8-dot table");
+ Assert.All(result.OutputDots78.Skip(1), d => Assert.False(d, "small letters should not"));
+ }
+
+ ///
+ /// liblouis only computes the information when a typeform buffer is supplied, so without one
+ /// the property must be null rather than a fabricated all-false array.
+ ///
+ [Fact]
+ public void IsNullWhenNoTypeFormWasPassed()
+ {
+ TranslatedString result = LibLouis.Instance.Translate(
+ EightDotTables(),
+ "Abc",
+ 16,
+ null,
+ null,
+ new int[3],
+ new int[16],
+ 0,
+ TranslationMode.Regular);
+
+ Assert.Null(result.OutputDots78);
+ }
+
+ ///
+ /// The safety half of the contract, restated from the caller's side: surfacing the output
+ /// information must not bring back the write-back into the caller's array.
+ ///
+ [Fact]
+ public void CallersArrayStaysUntouched()
+ {
+ TypeForm[] typeform = new TypeForm[3];
+ Array.Fill(typeform, TypeForm.Italic);
+
+ TranslatedString result = LibLouis.Instance.Translate(
+ EightDotTables(),
+ "Abc",
+ 16,
+ typeform,
+ null,
+ new int[3],
+ new int[16],
+ 0,
+ TranslationMode.Regular);
+
+ Assert.NotNull(result.OutputDots78);
+ Assert.All(typeform, t => Assert.Equal(TypeForm.Italic, t));
+ }
+}
diff --git a/LibLouis.NET.Test/PositionMappingTests.cs b/LibLouis.NET.Test/PositionMappingTests.cs
new file mode 100644
index 0000000..9ec1d80
--- /dev/null
+++ b/LibLouis.NET.Test/PositionMappingTests.cs
@@ -0,0 +1,181 @@
+using System;
+using System.IO;
+using System.Linq;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// liblouis indexes its position arrays in widechars - whole Unicode characters on our UCS-4
+/// builds. .NET callers read them as indices into a string, which is UTF-16. The two agree for
+/// BMP text and diverge on the first non-BMP character, so the wrapper translates them.
+///
+///
+/// The invariant that matters is not "the numbers look right" but that every value is directly
+/// usable as a string index: Output[result.InputPosition[k]] must address the character
+/// liblouis meant, and must never land on the trailing half of a surrogate pair.
+///
+public class PositionMappingTests
+{
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g26.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ private static TranslatedString Translate(string input)
+ {
+ int outputLength = Math.Max(16, input.Length * 4);
+
+ return LibLouis.Instance.Translate(
+ TablePaths(),
+ input,
+ outputLength,
+ null,
+ null,
+ new int[input.Length],
+ new int[outputLength],
+ 0,
+ TranslationMode.Regular);
+ }
+
+ ///
+ /// Sized to the strings they index, so no slicing guesswork is needed.
+ ///
+ [Theory]
+ [InlineData("Første linje. Anden linje.")]
+ [InlineData("bogstaver")]
+ [InlineData("a\U0001D11Eb")]
+ [InlineData("\U0001D11E\U0001D11E")]
+ [InlineData("😀 hej")]
+ public void ArraysAreSizedToTheStringsTheyIndex(string input)
+ {
+ TranslatedString result = Translate(input);
+
+ Assert.Equal(input.Length, result.OutputPosition.Length);
+ Assert.Equal(result.Output.Length, result.InputPosition.Length);
+ }
+
+ ///
+ /// Every InputPosition value must be a usable index into the input string, and must address
+ /// the start of a character rather than the low half of a surrogate pair.
+ ///
+ [Theory]
+ [InlineData("Første linje. Anden linje.")]
+ [InlineData("a\U0001D11Eb")]
+ [InlineData("\U0001D11E\U0001D11E")]
+ [InlineData("😀 hej")]
+ public void InputPositionsAddressWholeCharactersOfTheInput(string input)
+ {
+ TranslatedString result = Translate(input);
+
+ foreach (int position in result.InputPosition)
+ {
+ Assert.InRange(position, 0, input.Length - 1);
+ Assert.False(
+ char.IsLowSurrogate(input[position]),
+ $"position {position} lands on the trailing half of a surrogate pair");
+ }
+ }
+
+ ///
+ /// The same, in the other direction.
+ ///
+ [Theory]
+ [InlineData("Første linje. Anden linje.")]
+ [InlineData("a\U0001D11Eb")]
+ [InlineData("😀 hej")]
+ public void OutputPositionsAddressWholeCharactersOfTheOutput(string input)
+ {
+ TranslatedString result = Translate(input);
+
+ foreach (int position in result.OutputPosition)
+ {
+ Assert.InRange(position, 0, result.Output.Length - 1);
+ Assert.False(
+ char.IsLowSurrogate(result.Output[position]),
+ $"position {position} lands on the trailing half of a surrogate pair");
+ }
+ }
+
+ ///
+ /// Both halves of a surrogate pair belong to the same character, so both must report the same
+ /// braille cell.
+ ///
+ [Fact]
+ public void SurrogatePairHalvesShareAnOutputPosition()
+ {
+ const string input = "a\U0001D11Eb";
+
+ TranslatedString result = Translate(input);
+
+ // index 1 and 2 are the two halves of U+1D11E
+ Assert.Equal(result.OutputPosition[1], result.OutputPosition[2]);
+
+ // and the surrounding BMP characters map elsewhere
+ Assert.NotEqual(result.OutputPosition[0], result.OutputPosition[1]);
+ }
+
+ ///
+ /// BMP text must be completely unaffected: widechar and UTF-16 indices coincide there, so the
+ /// values have to match what liblouis wrote into the caller's scratch array.
+ ///
+ [Fact]
+ public void BmpTextIsUnchanged()
+ {
+ const string input = "Første linje. Anden linje, med kursiveret tekst. Tredje linje.";
+
+ int outputLength = input.Length * 4;
+
+ int[] scratchOutput = new int[input.Length];
+ int[] scratchInput = new int[outputLength];
+
+ TranslatedString result = LibLouis.Instance.Translate(
+ TablePaths(), input, outputLength, null, null, scratchOutput, scratchInput, 0, TranslationMode.Regular);
+
+ Assert.Equal(scratchOutput, result.OutputPosition);
+ Assert.Equal(scratchInput[..result.Output.Length], result.InputPosition);
+ }
+
+ ///
+ /// The pattern that consumer code actually uses, which was correct only for BMP text.
+ ///
+ [Theory]
+ [InlineData("Første linje")]
+ [InlineData("a\U0001D11Eb")]
+ public void ConsumerSlicePatternStaysInBounds(string input)
+ {
+ TranslatedString result = Translate(input);
+
+ int[] sliced = result.InputPosition[..result.Output.Length];
+
+ Assert.Equal(result.InputPosition.Length, sliced.Length);
+ Assert.All(sliced, p => Assert.InRange(p, 0, input.Length - 1));
+ }
+
+ ///
+ /// The cursor comes back as an index into the braille output, so it has to be translated too.
+ ///
+ [Fact]
+ public void CursorPositionIsAnIndexIntoTheOutput()
+ {
+ const string input = "a\U0001D11Ebc";
+
+ int outputLength = input.Length * 4;
+
+ // Cursor on 'b', which sits after the surrogate pair.
+ TranslatedString result = LibLouis.Instance.Translate(
+ TablePaths(),
+ input,
+ outputLength,
+ null,
+ null,
+ new int[input.Length],
+ new int[outputLength],
+ input.IndexOf('b', StringComparison.Ordinal),
+ TranslationMode.Regular);
+
+ Assert.InRange(result.CursorPosition, 0, result.Output.Length - 1);
+ Assert.False(char.IsLowSurrogate(result.Output[result.CursorPosition]));
+ }
+}
diff --git a/LibLouis.NET.Test/ReturnedStringOwnershipTests.cs b/LibLouis.NET.Test/ReturnedStringOwnershipTests.cs
new file mode 100644
index 0000000..4c3a811
--- /dev/null
+++ b/LibLouis.NET.Test/ReturnedStringOwnershipTests.cs
@@ -0,0 +1,41 @@
+using System;
+using System.IO;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// liblouis owns every string it returns, so the wrapper must not hand those pointers to the
+/// marshaller's Free.
+///
+/// * lou_setDataPath / lou_getDataPath return a pointer into a static char[MAXSTRING] inside
+/// liblouis (compileTranslationTable.c:59-73). Passing that to free() is undefined behaviour
+/// on every platform.
+/// * lou_findTable returns malloc'd memory. Our Windows binaries are built with mingw-w64 and
+/// allocate from msvcrt.dll, while .NET frees through ucrtbase.dll - different heaps, so
+/// freeing it from managed code corrupts the heap there.
+///
+public class ReturnedStringOwnershipTests
+{
+ ///
+ /// Setting the data path returns the static buffer, which the marshaller would then free.
+ ///
+ ///
+ /// The path is the test output directory rather than something arbitrary, because the data
+ /// path takes part in resolving relative table names and other tests rely on that.
+ ///
+ [Fact]
+ public void DataPath_RoundTripsWithoutFreeingLiblouisMemory()
+ {
+ string path = Path.TrimEndingDirectorySeparator(AppContext.BaseDirectory);
+
+ LibLouis.Instance.DataPath = path;
+
+ Assert.Equal(path, LibLouis.Instance.DataPath);
+
+ // Reading it again returns the same static buffer; a stale free shows up here as a crash
+ // or as garbage.
+ Assert.Equal(path, LibLouis.Instance.DataPath);
+ }
+}
diff --git a/LibLouis.NET.Test/SafeNativeMethods.cs b/LibLouis.NET.Test/SafeNativeMethods.cs
new file mode 100644
index 0000000..0bcf339
--- /dev/null
+++ b/LibLouis.NET.Test/SafeNativeMethods.cs
@@ -0,0 +1,41 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace LibLouis.NET.Test;
+
+///
+/// Raw P/Invoke used to characterise native behaviour without going through the wrapper.
+///
+///
+/// Strings are passed as pre-encoded NUL terminated UTF-8 rather than as managed strings, so
+/// there is no marshalling behaviour of our own between the test and liblouis.
+///
+internal static class SafeNativeMethods
+{
+ ///
+ /// Bytes per liblouis widechar: 2 for a UCS-2 build, 4 for UCS-4.
+ ///
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ [DllImport("liblouis", EntryPoint = "lou_charSize")]
+ internal static extern int lou_charSize();
+
+ [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
+ [DllImport("liblouis", EntryPoint = "lou_translateString")]
+ internal static extern int lou_translateString(
+ byte[] tableList,
+ byte[] inbuf,
+ ref int inlen,
+ byte[] outbuf,
+ ref int outlen,
+ ushort[]? typeform,
+ byte[]? spacing,
+ int mode);
+
+ ///
+ /// Encodes a string the way liblouis expects a const char *.
+ ///
+ internal static byte[] Utf8(string value)
+ {
+ return Encoding.UTF8.GetBytes(value + "\0");
+ }
+}
diff --git a/LibLouis.NET.Test/ShutdownTests.cs b/LibLouis.NET.Test/ShutdownTests.cs
new file mode 100644
index 0000000..f7ccec0
--- /dev/null
+++ b/LibLouis.NET.Test/ShutdownTests.cs
@@ -0,0 +1,170 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// Shutdown frees liblouis's translation and display table chains, which are global to the
+/// process. It is deliberately not IDisposable: nothing here is owned by a single caller, so
+/// there is no "done with it" moment to hang disposal off, and an accidental
+/// using (LibLouis.Instance) would tear liblouis down for everything else in the process.
+///
+///
+/// These tests set the flag directly instead of calling Shutdown. LibLouis is a process-wide
+/// singleton and the suite runs serially in one process, so really shutting it down would fail
+/// every test that ran afterwards. The flag is restored in a finally for the same reason.
+/// End-to-end shutdown is exercised out of process.
+///
+public class ShutdownTests
+{
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g26.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ private static FieldInfo ShutDownField =>
+ typeof(LibLouis).GetField("_shutDown", BindingFlags.NonPublic | BindingFlags.Static)
+ ?? throw new InvalidOperationException("_shutDown field not found");
+
+ ///
+ /// The shape change itself: an accidental using statement must not compile.
+ ///
+ [Fact]
+ public void LibLouisIsNotDisposable()
+ {
+ Assert.False(typeof(IDisposable).IsAssignableFrom(typeof(LibLouis)));
+ }
+
+ ///
+ /// Shutdown is process-wide teardown, so it belongs on the type, not on an instance nobody
+ /// exclusively owns.
+ ///
+ [Fact]
+ public void ShutdownIsStatic()
+ {
+ MethodInfo? shutdown = typeof(LibLouis).GetMethod(
+ "Shutdown", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+
+ Assert.NotNull(shutdown);
+ Assert.Equal(typeof(void), shutdown.ReturnType);
+ }
+
+ [Fact]
+ public void UsingTheInstanceAfterShutdownThrows()
+ {
+ WhileMarkedShutDown(() =>
+ {
+ Assert.Throws(
+ () => LibLouis.Instance.Translate(TablePaths(), "abc", 16, null, null, TranslationMode.Regular));
+
+ Assert.Throws(
+ () => LibLouis.Instance.Translate(
+ TablePaths(), "abc", 16, null, null, new int[16], new int[16], 0, TranslationMode.Regular));
+
+ Assert.Throws(
+ () => LibLouis.Instance.BackTranslate(TablePaths(), "abc", 16, null, null, TranslationMode.Regular));
+
+ Assert.Throws(
+ () => LibLouis.Instance.CharactersToDots(TablePaths(), "abc"));
+
+ Assert.Throws(
+ () => LibLouis.Instance.DotsToCharacters(TablePaths(), "abc"));
+
+ Assert.Throws(
+ () => LibLouis.Instance.Hyphenate(TablePaths(), "bogstaver", TranslationMode.Regular));
+
+ Assert.Throws(
+ () => LibLouis.Instance.IndexTables(TablePaths()));
+
+ Assert.Throws(
+ () => LibLouis.Instance.FindTable("type:literary"));
+ });
+ }
+
+ ///
+ /// The message has to say what happened: "cannot access a disposed object" would be a lie for
+ /// a type that is not disposable.
+ ///
+ [Fact]
+ public void TheFailureExplainsItself()
+ {
+ WhileMarkedShutDown(() =>
+ {
+ InvalidOperationException e = Assert.Throws(
+ () => LibLouis.Instance.CharactersToDots(TablePaths(), "abc"));
+
+ Assert.Contains("shut down", e.Message, StringComparison.OrdinalIgnoreCase);
+ });
+ }
+
+ ///
+ /// Diagnostics stay available: neither touches anything lou_free released.
+ ///
+ [Fact]
+ public void VersionAndLoggerSurviveShutdown()
+ {
+ WhileMarkedShutDown(() =>
+ {
+ Assert.False(string.IsNullOrWhiteSpace(LibLouis.Instance.Version));
+
+ LibLouis.Instance.Logger = new CollectingLogger();
+ });
+ }
+
+ ///
+ /// The instance works again once the flag is cleared, so the guard is the only thing stopping
+ /// it - the test is not just observing a broken singleton.
+ ///
+ [Fact]
+ public void TheGuardIsWhatBlocksUse()
+ {
+ WhileMarkedShutDown(() =>
+ Assert.Throws(
+ () => LibLouis.Instance.CharactersToDots(TablePaths(), "abc")));
+
+ Assert.Equal(3, LibLouis.Instance.CharactersToDots(TablePaths(), "abc").Length);
+ }
+
+ ///
+ /// lou_free is process-global, but a finalizer is per managed instance. In a collectible
+ /// AssemblyLoadContext that would free the tables of every other context still using them,
+ /// and it would do it on the finalizer thread, outside the lock.
+ ///
+ [Fact]
+ public void LibLouisHasNoFinalizer()
+ {
+ MethodInfo? finalizer = typeof(LibLouis)
+ .GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ Assert.Equal(typeof(object), finalizer?.DeclaringType);
+ }
+
+ [Fact]
+ public void ShutDownFlagIsVolatile()
+ {
+ // Read outside the lock by the guards, written under it by Shutdown.
+ Assert.Contains(
+ ShutDownField.GetRequiredCustomModifiers(),
+ m => m == typeof(System.Runtime.CompilerServices.IsVolatile));
+ }
+
+ private static void WhileMarkedShutDown(Action body)
+ {
+ FieldInfo field = ShutDownField;
+
+ field.SetValue(null, true);
+
+ try
+ {
+ body();
+ }
+ finally
+ {
+ field.SetValue(null, false);
+ }
+ }
+}
diff --git a/LibLouis.NET.Test/TypeFormBufferTests.cs b/LibLouis.NET.Test/TypeFormBufferTests.cs
new file mode 100644
index 0000000..3d01cf2
--- /dev/null
+++ b/LibLouis.NET.Test/TypeFormBufferTests.cs
@@ -0,0 +1,129 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// liblouis writes the typeform array back for every *output* cell, not for every input
+/// character. Passing a typeform array sized to the input therefore lets native code write
+/// past the end of a managed array whenever the translation grows the text - which the
+/// marker tables in this repository do routinely.
+///
+public class TypeFormBufferTests
+{
+ private const string Input = "This is a test.";
+
+ /// Translation of with the marker tables, 20 cells for 15 characters.
+ private const string ExpectedOutput = "`,@this is a test.`,";
+
+ private static readonly string[] Tables = ["da-dk-braillo.dis", "da-dk-g16-markers.ctb"];
+
+ private static string[] TablePaths() =>
+ [.. Tables.Select(t => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nota-tables", t))];
+
+ ///
+ /// Documents the native contract that makes the overrun possible, independent of the wrapper:
+ /// lou_translateString writes one typeform entry per output cell. Uses a deliberately
+ /// oversized buffer so nothing is corrupted while we measure how far native code writes.
+ ///
+ [Fact]
+ public void Native_WritesOneTypeformEntryPerOutputCell()
+ {
+ int charSize = SafeNativeMethods.lou_charSize();
+ Encoding encoder = charSize == 4 ? Encoding.UTF32 : Encoding.Unicode;
+
+ int inputLength = Input.Length;
+ int outputLength = ExpectedOutput.Length;
+
+ byte[] inputBuffer = encoder.GetBytes(Input + "\0");
+ byte[] outputBuffer = new byte[(outputLength + 1) * charSize];
+
+ // Far larger than either length, so native writes stay in bounds and are observable.
+ // typeform is in/out: the first inputLength entries are real input (foreign language,
+ // matching the other tests), the rest are plain text. Neither value collides with the
+ // ASCII '0' / '8' that liblouis writes back, so any such entry marks a native write.
+ ushort[] typeform = new ushort[outputLength * 4];
+ Array.Fill(typeform, (ushort)TypeForm.ForeignLanguage, 0, inputLength);
+
+ int inLen = inputLength;
+ int outLen = outputLength;
+
+ int result = SafeNativeMethods.lou_translateString(
+ SafeNativeMethods.Utf8(string.Join(',', TablePaths())),
+ inputBuffer,
+ ref inLen,
+ outputBuffer,
+ ref outLen,
+ typeform,
+ null,
+ 0);
+
+ Assert.NotEqual(0, result);
+ Assert.Equal(ExpectedOutput, encoder.GetString(outputBuffer, 0, outLen * charSize));
+
+ // liblouis writes the ASCII characters '0' / '8' per output cell.
+ int lastWritten = Array.FindLastIndex(typeform, t => t == '0' || t == '8');
+
+ Assert.Equal(outLen - 1, lastWritten);
+
+ // The point of the test: native wrote beyond the input length, so an input-sized
+ // managed array would have been overrun by exactly this many entries.
+ Assert.True(
+ lastWritten >= inputLength,
+ $"Expected native writes past input length {inputLength}, but last write was at {lastWritten}.");
+ }
+
+ ///
+ /// The wrapper must not let native code write into - let alone past - the caller's typeform
+ /// array. The public contract sizes typeform to the input, so the wrapper owes the caller a
+ /// buffer big enough for the output.
+ ///
+ [Fact]
+ public void Translate_DoesNotWriteIntoCallersTypeformArray()
+ {
+ TypeForm[] typeform = new TypeForm[Input.Length];
+ Array.Fill(typeform, TypeForm.ForeignLanguage);
+
+ TypeForm[] untouched = (TypeForm[])typeform.Clone();
+
+ string output = LibLouis.Instance.Translate(
+ TablePaths(), Input, Input.Length * 2, typeform, null, TranslationMode.Regular);
+
+ Assert.Equal(ExpectedOutput, output);
+ Assert.Equal(untouched, typeform);
+ }
+
+ ///
+ /// The same overrun through the position-reporting overload.
+ ///
+ [Fact]
+ public void TranslateWithPositions_DoesNotWriteIntoCallersTypeformArray()
+ {
+ int outputLength = Input.Length * 2;
+
+ TypeForm[] typeform = new TypeForm[Input.Length];
+ Array.Fill(typeform, TypeForm.ForeignLanguage);
+
+ TypeForm[] untouched = (TypeForm[])typeform.Clone();
+
+ TranslatedString translated = LibLouis.Instance.Translate(
+ TablePaths(),
+ Input,
+ outputLength,
+ typeform,
+ null,
+ new int[Input.Length],
+ new int[outputLength],
+ 0,
+ TranslationMode.Regular);
+
+ Assert.Equal(ExpectedOutput, translated.Output);
+ Assert.Equal(untouched, typeform);
+ }
+
+}
diff --git a/LibLouis.NET.Test/UTF8StringNoFreeMarshallerTests.cs b/LibLouis.NET.Test/UTF8StringNoFreeMarshallerTests.cs
new file mode 100644
index 0000000..eea4522
--- /dev/null
+++ b/LibLouis.NET.Test/UTF8StringNoFreeMarshallerTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Xunit;
+
+namespace LibLouis.NET.Test;
+
+///
+/// The marshaller used for strings liblouis owns. It only converts inbound: not freeing is
+/// correct for memory liblouis allocated, and would be a leak for buffers we allocate ourselves,
+/// so it is restricted to return values.
+///
+public unsafe class UTF8StringNoFreeMarshallerTests
+{
+ [Theory]
+ [InlineData("")]
+ [InlineData("a")]
+ [InlineData("nota-tables/da-dk-g26.ctb")]
+ [InlineData("Første linje")] // multi-byte UTF-8
+ [InlineData("\U0001D11E")] // non-BMP, surrogate pair on the managed side
+ public void ConvertToManaged_ReadsNulTerminatedUtf8(string value)
+ {
+ byte[] utf8 = Encoding.UTF8.GetBytes(value + "\0");
+
+ fixed (byte* unmanaged = utf8)
+ {
+ Assert.Equal(value, UTF8StringNoFreeMarshaller.ConvertToManaged(unmanaged));
+ }
+ }
+
+ ///
+ /// The string must stop at the terminator, not run on into whatever follows it.
+ ///
+ [Fact]
+ public void ConvertToManaged_StopsAtTheTerminator()
+ {
+ byte[] utf8 = Encoding.UTF8.GetBytes("abc\0trailing garbage");
+
+ fixed (byte* unmanaged = utf8)
+ {
+ Assert.Equal("abc", UTF8StringNoFreeMarshaller.ConvertToManaged(unmanaged));
+ }
+ }
+
+ [Fact]
+ public void ConvertToManaged_MapsNullPointerToNull()
+ {
+ Assert.Null(UTF8StringNoFreeMarshaller.ConvertToManaged(null));
+ }
+
+ ///
+ /// Free must leave the memory alone. If it released it, the allocator would abort on the
+ /// second release here.
+ ///
+ [Fact]
+ public void Free_DoesNotReleaseTheMemory()
+ {
+ byte* buffer = (byte*)NativeMemory.Alloc(4);
+
+ UTF8StringNoFreeMarshaller.Free(buffer);
+
+ // Ours to release, and still ours after Free.
+ NativeMemory.Free(buffer);
+ }
+}
diff --git a/LibLouis.NET.Test/braille-specs/da-dk-6dot.yaml b/LibLouis.NET.Test/braille-specs/da-dk-6dot.yaml
new file mode 100644
index 0000000..c9a79a3
--- /dev/null
+++ b/LibLouis.NET.Test/braille-specs/da-dk-6dot.yaml
@@ -0,0 +1,1124 @@
+display: unicode-without-blank.dis
+
+# -------------
+# Grade 1 and 2
+# -------------
+
+# Round trip tests
+# Commented tests currently fail backwards but should be fixed.
+
+table: {language: da, grade: 1, dots: 6, version: 2022, __assert-match: da-dk-g16.ctb}
+table: {language: da, grade: 2, dots: 6, version: 2022, __assert-match: da-dk-g26.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠙"]
+ - ["%", "⠚⠴"]
+ - ["'", "⠈"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["[", "⠐⠦"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["{", "⠘⠪"]
+ - ["}", "⠘⠕"]
+ - ["~", "⠘⠆"]
+ - ["€", "⠘⠑"]
+ - ["ƒ", "⠘⠋"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠠⠐⠎"]
+ - ["Ž", "⠠⠐⠵"]
+ - ["•", "⠘⠄"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["©", "⠘⠉"]
+ - ["®", "⠘⠗"]
+ - ["°", "⠘⠴"]
+ - ["µ", "⠐⠍"]
+ - ["Ä", "⠠⠐⠜"]
+ - ["Ç", "⠠⠐⠉"]
+ - ["Î", "⠠⠐⠊"]
+ - ["Ð", "⠠⠐⠙"]
+ - ["Ñ", "⠠⠐⠝"]
+ - ["Ô", "⠠⠐⠕"]
+ - ["Ö", "⠠⠐⠪"]
+ - ["Û", "⠠⠐⠥"]
+ - ["Ý", "⠠⠐⠽"]
+ - ["Þ", "⠠⠐⠞"]
+ - ["ç", "⠐⠉"]
+ - ["î", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ô", "⠐⠕"]
+ - ["ö", "⠐⠪"]
+ - ["û", "⠐⠥"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+
+# Misc. Unicode (most cannot be back-translated)
+# Some tests may be repititions of tests above, since
+# some characters can occur both inside and outside the 8 bit range.
+# for accented letters in the range u+0080 - u+00ff: the letters
+# that are thought to occur most frequently in Danish texts are used
+# for back-translation.
+# For all accented letters above u+00ff, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ["\u0134\u0135", "⠠⠐⠚⠐⠚"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0192", "⠘⠋"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0392\u03b2", "⠠⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠠⠐⠛⠐⠛"]
+ - ["\u0397\u03b7", "⠠⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠠⠐⠹⠐⠹"]
+ - ["\u039a\u03ba", "⠠⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠠⠐⠇⠐⠇"]
+ - ["\u039e\u03be", "⠠⠐⠭⠐⠭"]
+ - ["\u03a0\u03c0", "⠠⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠠⠐⠗⠐⠗"]
+ - ["\u03a6\u03c6", "⠠⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠠⠐⠓⠐⠓"]
+ - ["\u03a9\u03c9", "⠠⠐⠺⠐⠺"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2193", "⠘⠟"]
+
+# Section sign
+
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+ - ["§§ 3-5", "⠬⠬⠼⠉⠤⠼⠑"]
+
+# Caps and mixed case
+
+ - ["Foobar", "⠠⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠰⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠰⠃⠁⠗"]
+
+# Times vs. bullit
+
+ - ["\u2022Bullit", "⠘⠄⠠⠃⠥⠇⠇⠊⠞"]
+# - ["2 \u00d7 2 = 4", "⠼⠃ ⠘⠄ ⠼⠃ ⠘⠶ ⠼⠙"]
+
+# Numbers and punctuation
+
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+# - [" -", " ⠤⠤"]
+ - [" - ", " ⠤⠤ "]
+
+# Digits and letters
+
+ - ["1a", "⠼⠁⠰⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠰⠁"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+# Single characters
+
+ - ['\x0009', ' ']
+ - ['\x000a', ' ']
+ - ['\x000b', ' ']
+ - ['\x000c', ' ']
+ - ['\x000d', ' ']
+ - ["`", "⠈"]
+ - ["‚", "⠈"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‹", "⠈"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["Œ", "⠠⠕⠑"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠆"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["Ÿ", "⠠⠐⠽"]
+ - ['\x00a0', ' ']
+ - ["«", "⠶"]
+ - ["±", "⠘⠖⠤"]
+ - ["´", "⠈"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["Á", "⠠⠐⠁"]
+ - ["Â", "⠠⠐⠁"]
+ - ["Ã", "⠠⠐⠁"]
+ - ["Ë", "⠠⠐⠑"]
+ - ["Ì", "⠠⠐⠊"]
+ - ["Í", "⠠⠐⠊"]
+ - ["Ï", "⠠⠐⠊"]
+ - ["Ò", "⠠⠐⠕"]
+ - ["Ó", "⠠⠐⠕"]
+ - ["Õ", "⠠⠐⠕"]
+ - ["Ù", "⠠⠐⠥"]
+ - ["Ú", "⠠⠐⠥"]
+ - ["ß", "⠎⠎"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠠⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠠⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠠⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠠⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠠⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠠⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠠⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠠⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠠⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠠⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠠⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠠⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠠⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠠⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠠⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠠⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠠⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠠⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠠⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠠⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠠⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠠⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠠⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠠⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠠⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠠⠊⠚⠊⠚"]
+ - ["\u0136\u0137", "⠠⠐⠅⠐⠅"]
+ - ["\u0139\u013a", "⠠⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠠⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠠⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠠⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠠⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠠⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠠⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠠⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠠⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠠⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠠⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠠⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠠⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠠⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠠⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠠⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠠⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠠⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠠⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠠⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠠⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠠⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠠⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠠⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠠⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠠⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠠⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠠⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠠⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠠⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠠⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠠⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠠⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠠⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠠⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u02dc", "⠘⠆"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠠⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠠⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠠⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠠⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠠⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠠⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠠⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠠⠐⠁⠐⠁"]
+ - ["\u0394\u03b4", "⠠⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠠⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠠⠐⠵⠐⠵"]
+ - ["\u0399\u03b9", "⠠⠐⠊⠐⠊"]
+ - ["\u039c\u03bc", "⠠⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠠⠐⠝⠐⠝"]
+ - ["\u039f\u03bf", "⠠⠐⠕⠐⠕"]
+ - ["\u03a3\u03c3", "⠠⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠠⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠠⠐⠥⠐⠥"]
+ - ["\u03a8\u03c8", "⠠⠐⠽⠐⠽"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2192", "⠘⠗"] # back-translates as "registered".
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Extra digits
+
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+
+# Characters and constructs which cannot be properly back-translated in grade 2
+
+table: {language: da, grade: 1, dots: 6, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["¹", "⠼⠬⠁"]
+ - ["Ê", "⠠⠐⠑"]
+ - ["×", "⠘⠔"]
+ - ["ê", "⠐⠑"]
+ - ["÷", "⠘⠲"]
+
+# same tests but forward direction only
+table: {language: da, grade: 2, dots: 6, version: 2022}
+flags: {testmode: forward}
+tests:
+
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["¹", "⠼⠬⠁"]
+ - ["Ê", "⠠⠐⠑"]
+ - ["×", "⠘⠔"]
+ - ["ê", "⠐⠑"]
+ - ["÷", "⠘⠲"]
+
+# Braille patterns which back-translate to something other than the original
+# Typically, single character representations back-translating to strings with more than one character
+
+table: {language: da, grade: 1, dots: 6, version: 2022}
+table: {language: da, grade: 2, dots: 6, version: 2022}
+flags: {testmode: backward}
+tests:
+
+# Characters
+
+ - ["⠄⠄⠄", "..."]
+ - ["⠠⠕⠑", "Oe"]
+ - ["⠕⠑", "oe"]
+ - ["⠘⠖⠤", "±", {xfail: true}]
+ - ["⠼⠁⠌⠙", "1/4"]
+ - ["⠼⠁⠌⠃", "1/2"]
+ - ["⠼⠉⠌⠙", "3/4"]
+
+# Extra digits
+
+ - ["⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙", "1\u00bc + 1\u00bd = 2\u00be", {xfail: true}]
+
+# -------
+# Grade 1
+# -------
+
+# Round trip tests
+# Commented tests currently fail backwards but should be fixed.
+
+table: {language: da, grade: 1, dots: 6, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters
+
+ - ["&", "⠯"]
+ - ["(", "⠦"]
+ - [")", "⠴"]
+ - ["*", "⠔"]
+ - ["/", "⠌"]
+ - ["A", "⠠⠁"]
+ - ["B", "⠠⠃"]
+ - ["C", "⠠⠉"]
+ - ["D", "⠠⠙"]
+ - ["E", "⠠⠑"]
+ - ["F", "⠠⠋"]
+ - ["G", "⠠⠛"]
+ - ["H", "⠠⠓"]
+ - ["I", "⠠⠊"]
+ - ["J", "⠠⠚"]
+ - ["K", "⠠⠅"]
+ - ["L", "⠠⠇"]
+ - ["M", "⠠⠍"]
+ - ["N", "⠠⠝"]
+ - ["O", "⠠⠕"]
+ - ["P", "⠠⠏"]
+ - ["Q", "⠠⠟"]
+ - ["R", "⠠⠗"]
+ - ["S", "⠠⠎"]
+ - ["T", "⠠⠞"]
+ - ["U", "⠠⠥"]
+ - ["V", "⠠⠧"]
+ - ["W", "⠠⠺"]
+ - ["X", "⠠⠭"]
+ - ["Y", "⠠⠽"]
+ - ["Z", "⠠⠵"]
+ - ["\u005c\u005c", "⠘⠌"]
+ - ["a", "⠁"]
+ - ["b", "⠃"]
+ - ["c", "⠉"]
+ - ["d", "⠙"]
+ - ["e", "⠑"]
+ - ["f", "⠋"]
+ - ["g", "⠛"]
+ - ["h", "⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠚"]
+ - ["k", "⠅"]
+ - ["l", "⠇"]
+ - ["m", "⠍"]
+ - ["n", "⠝"]
+ - ["o", "⠕"]
+ - ["p", "⠏"]
+ - ["q", "⠟"]
+ - ["r", "⠗"]
+ - ["s", "⠎"]
+ - ["t", "⠞"]
+ - ["u", "⠥"]
+ - ["v", "⠧"]
+ - ["w", "⠺"]
+ - ["x", "⠭"]
+ - ["y", "⠽"]
+ - ["z", "⠵"]
+ - ["|", "⠘⠧"]
+ - ["ž", "⠐⠵"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["§", "⠬"]
+ - ["À", "⠠⠷"]
+ - ["Å", "⠠⠡"]
+ - ["Æ", "⠠⠜"]
+ - ["È", "⠠⠮"]
+ - ["É", "⠠⠿"]
+ - ["Ø", "⠠⠪"]
+ - ["Ü", "⠠⠳"]
+ - ["à", "⠷"]
+ - ["å", "⠡"]
+ - ["ä", "⠐⠜"]
+ - ["æ", "⠜"]
+ - ["è", "⠮"]
+ - ["é", "⠿"]
+ - ["ø", "⠪"]
+ - ["ü", "⠳"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠠⠟⠥⠊⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍⠑⠙ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⠠⠺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏⠡ ⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Numbers and punctuation
+
+ - ["J) j) %) ') \u2030)", "⠠⠚⠰⠴ ⠚⠰⠴ ⠚⠴⠰⠴ ⠈⠴ ⠚⠴⠴⠰⠴"]
+ - ["\"quotes\"", "⠶⠟⠥⠕⠞⠑⠎⠶"]
+ - [":-) :-(", "⠒⠤⠴ ⠒⠤⠦"]
+ - [";-) ;-(", "⠆⠤⠴ ⠆⠤⠦"]
+ - [" a-", " ⠁⠤"]
+ - [" -a-", " ⠤⠁⠤"]
+ - ["(parentheses)", "⠦⠏⠁⠗⠑⠝⠞⠓⠑⠎⠑⠎⠴"]
+
+# Multi-pass tests
+
+# - ["~x |z", "⠘⠰⠭ ⠘⠸⠵"]
+# - ["~X |Z", "⠘⠰⠠⠭ ⠘⠸⠠⠵"]
+ - ["5É", "⠼⠑⠠⠿"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+# Single characters
+
+ - ["¡", "⠲"]
+ - ["", "⠤⠤"]
+ - ["¯", "⠢"]
+
+# Apostrophes
+
+ - ["\u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠚⠴⠴⠰⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴ ⠈⠴"]
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠨⠠⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠋⠑⠙⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠠⠑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠠⠑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠨⠠⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄⠨
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠠⠑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠠⠑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠨⠠⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠨
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠠⠑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠠⠑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠨⠠⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠁⠇⠞⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠠⠑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠠⠑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# -------
+# Grade 2
+# -------
+
+# Round trip tests
+# Commented tests currently fail backwards but should be fixed.
+
+table: {language: da, grade: 2, dots: 6, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters
+
+ - ["&", "⠰⠯"]
+ - ["(", "⠰⠦"]
+ - [")", "⠰⠴"]
+ - ["*", "⠰⠔"]
+ - ["/", "⠰⠌", {xfail: {forward: true}}]
+ - ["A", "⠰⠠⠁"]
+ - ["B", "⠰⠠⠃"]
+ - ["C", "⠰⠠⠉"]
+ - ["D", "⠰⠠⠙"]
+ - ["E", "⠰⠠⠑"]
+ - ["F", "⠰⠠⠋"]
+ - ["G", "⠰⠠⠛"]
+ - ["H", "⠰⠠⠓"]
+ - ["I", "⠠⠊"]
+ - ["J", "⠰⠠⠚"]
+ - ["K", "⠰⠠⠅"]
+ - ["L", "⠰⠠⠇"]
+ - ["M", "⠰⠠⠍"]
+ - ["N", "⠰⠠⠝"]
+ - ["O", "⠰⠠⠕"]
+ - ["P", "⠰⠠⠏"]
+ - ["Q", "⠰⠠⠟"]
+ - ["R", "⠰⠠⠗"]
+ - ["S", "⠰⠠⠎"]
+ - ["T", "⠰⠠⠞"]
+ - ["U", "⠰⠠⠥"]
+ - ["V", "⠰⠠⠧"]
+ - ["W", "⠰⠠⠺"]
+ - ["X", "⠰⠠⠭"]
+ - ["Y", "⠰⠠⠽"]
+ - ["Z", "⠰⠠⠵"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["a", "⠰⠁"]
+ - ["b", "⠰⠃"]
+ - ["c", "⠰⠉"]
+ - ["d", "⠰⠙"]
+ - ["e", "⠰⠑"]
+ - ["f", "⠰⠋"]
+ - ["g", "⠰⠛"]
+ - ["h", "⠰⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠰⠚"]
+ - ["k", "⠰⠅"]
+ - ["l", "⠰⠇"]
+ - ["m", "⠰⠍"]
+ - ["n", "⠰⠝"]
+ - ["o", "⠰⠕"]
+ - ["p", "⠰⠏"]
+ - ["q", "⠰⠟"]
+ - ["r", "⠰⠗"]
+ - ["s", "⠰⠎"]
+ - ["t", "⠰⠞"]
+ - ["u", "⠰⠥"]
+ - ["v", "⠰⠧"]
+ - ["w", "⠰⠺"]
+ - ["x", "⠰⠭"]
+ - ["y", "⠰⠽"]
+ - ["z", "⠰⠵"]
+ - ["|", "⠘⠸"]
+ - ["ž", "⠐⠵"]
+ - ["¡", "⠰⠲"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["À", "⠰⠠⠷"]
+ - ["Å", "⠰⠠⠡"]
+ - ["Æ", "⠰⠠⠜"]
+ - ["È", "⠰⠠⠮"]
+ - ["É", "⠰⠠⠿"]
+ - ["Ø", "⠰⠠⠪"]
+ - ["Ü", "⠰⠠⠳"]
+ - ["à", "⠰⠷"]
+ - ["ä", "⠐⠜"]
+ - ["å", "⠰⠡"]
+ - ["æ", "⠰⠜"]
+ - ["è", "⠰⠮"]
+ - ["é", "⠰⠿"]
+ - ["ø", "⠰⠪"]
+ - ["ü", "⠰⠳"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠰⠠⠟⠥⠊⠰⠵⠹⠇⠞⠁⠛⠱⠫ ⠎⠏⠊⠵⠑ ⠚⠭⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠹⠂ ⠍⠣⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠣ ⠰⠠⠺⠁⠇⠞⠓⠱ ⠎⠏⠊⠇⠇⠑⠹ ⠏ ⠰⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Percent and permille
+
+ - ["1%", "⠼⠁ ⠚⠴"]
+
+# Numbers and punctuation
+
+# - ["2\u00d72", "⠼⠃⠘⠄⠼⠃"]
+ - ["\"quotes\"", "⠶⠰⠟⠥⠕⠳⠎⠶"]
+ - [":-) :-(", "⠒⠤⠰⠴ ⠒⠤⠰⠦"]
+ - [";-) ;-(", "⠆⠤⠰⠴ ⠆⠤⠰⠦"]
+ - [" a-", " ⠰⠁⠤"]
+ - [" -a-", " ⠤⠰⠁⠤"]
+ - ["(parentheses)", "⠦⠏⠁⠗⠣⠞⠓⠑⠎⠑⠎⠴"]
+
+# Exclamation
+
+ - ["\u00a1Que lastima!", "⠰⠲⠰⠠⠟⠥⠑ ⠇⠁⠵⠊⠍⠁⠖"]
+
+# URLs emails and file names
+
+ - ["$at", "⠘⠙⠁⠞"]
+ - ["\u005c\u005cat\u005c\u005cbliver", "⠘⠡⠁⠞⠘⠡⠃⠇⠊⠧⠑⠗"]
+ - ["at@bliver.og", "⠁⠞⠘⠁⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["http://at.bliver.og", "⠓⠞⠞⠏⠒⠌⠌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.og", "⠺⠺⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.com", "⠺⠺⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍"]
+ - ["www.a.b.c", "⠺⠺⠺⠄⠰⠁⠄⠰⠃⠄⠰⠉"]
+
+# Word contractions
+
+ - ["At", "⠠⠁"]
+ - ["at", "⠁"]
+ - ["Aldrig", "⠠⠁⠔"]
+ - ["aldrig", "⠁⠔"]
+ - ["aig", "⠁⠊⠛"]
+ - ["Alle", "⠠⠁⠑"]
+ - ["alle", "⠁⠑"]
+ - ["ae", "⠰⠁⠑"]
+ - ["Allerede", "⠠⠁⠇⠗"]
+ - ["allerede", "⠁⠇⠗"]
+ - ["alr", "⠰⠁⠇⠗"]
+ - ["Alligevel", "⠠⠁⠇⠧"]
+ - ["alligevel", "⠁⠇⠧"]
+ - ["alv", "⠰⠁⠇⠧"]
+ - ["Altid", "⠠⠁⠞⠙"]
+ - ["altid", "⠁⠞⠙"]
+ - ["atd", "⠰⠁⠞⠙"]
+ - ["Altså", "⠠⠁⠡"]
+ - ["altså", "⠁⠡"]
+ - ["aå", "⠰⠁⠡"]
+ - ["Bliver", "⠠⠃"]
+ - ["bliver", "⠃"]
+ - ["Og", "⠠⠉"]
+ - ["og", "⠉"]
+ - ["Deres", "⠠⠲"]
+ - ["deres", "⠲"]
+ - ["Du", "⠠⠙"]
+ - ["du", "⠙"]
+ - ["Eller", "⠠⠑"]
+ - ["eller", "⠑"]
+ - ["For", "⠠⠋"]
+ - ["for", "⠋"]
+ - ["Gør", "⠠⠛"]
+ - ["gør", "⠛"]
+ - ["Har", "⠠⠓"]
+ - ["har", "⠓"]
+ - ["Jeg", "⠠⠚"]
+ - ["jeg", "⠚"]
+ - ["Kan", "⠠⠅"]
+ - ["kan", "⠅"]
+ - ["Lige", "⠠⠇"]
+ - ["lige", "⠇"]
+ - ["Med", "⠠⠍"]
+ - ["med", "⠍"]
+ - ["Når", "⠠⠝"]
+ - ["når", "⠝"]
+ - ["Op", "⠠⠕"]
+ - ["op", "⠕"]
+ - ["På", "⠠⠏"]
+ - ["på", "⠏"]
+ - ["Under", "⠠⠟"]
+ - ["under", "⠟"]
+ - ["Rigtig", "⠠⠗"]
+ - ["rigtig", "⠗"]
+ - ["Som", "⠠⠎"]
+ - ["som", "⠎"]
+ - ["Til", "⠠⠞"]
+ - ["til", "⠞"]
+ - ["Hun", "⠠⠥"]
+ - ["hun", "⠥"]
+ - ["Ved", "⠠⠧"]
+ - ["ved", "⠧"]
+ - ["Hvad", "⠠⠺"]
+ - ["hvad", "⠺"]
+ - ["Over", "⠠⠭"]
+ - ["over", "⠭"]
+ - ["Han", "⠠⠽"]
+ - ["han", "⠽"]
+ - ["Efter", "⠠⠵"]
+ - ["efter", "⠵"]
+ - ["Være", "⠠⠜"]
+ - ["være", "⠜"]
+ - ["Før", "⠠⠪"]
+ - ["før", "⠪"]
+ - ["Så", "⠠⠡"]
+ - ["så", "⠡"]
+ - ["Den", "⠠⠯"]
+ - ["den", "⠯"]
+ - ["Der", "⠠⠾"]
+ - ["der", "⠾"]
+ - ["Det", "⠠⠮"]
+ - ["det", "⠮"]
+ - ["De", "⠠⠹"]
+ - ["de", "⠹"]
+ - ["En", "⠠⠣"]
+ - ["en", "⠣"]
+ - ["Er", "⠠⠱"]
+ - ["er", "⠱"]
+ - ["Et", "⠠⠬"]
+ - ["et", "⠬"]
+ - ["Gennem", "⠠⠻"]
+ - ["gennem", "⠻"]
+ - ["Hvor", "⠠⠌"]
+ - ["hvor", "⠌"]
+ - ["Men", "⠠⠩"]
+ - ["men", "⠩"]
+ - ["Ned", "⠠⠫"]
+ - ["ned", "⠫"]
+ - ["Ret", "⠠⠷"]
+ - ["ret", "⠷"]
+ - ["Skal", "⠠⠿"]
+ - ["skal", "⠿"]
+ - ["Te", "⠠⠳"]
+ - ["te", "⠳"]
+ - ["Var", "⠠⠼"]
+ - ["var", "⠼"]
+ - ["Ve", "⠠⠧⠑"]
+ - ["ve", "⠧⠑"]
+
+# Partword/nocross
+
+ - ["Denne", "⠠⠯⠫"]
+ - ["denne", "⠯⠫"]
+ - ["Mændene", "⠠⠍⠜⠝⠹⠫"]
+ - ["mændene", "⠍⠜⠝⠹⠫"]
+ - ["Derhos", "⠠⠾⠓⠕⠎"]
+ - ["derhos", "⠾⠓⠕⠎"]
+ - ["Hunderace", "⠠⠓⠥⠝⠹⠗⠁⠉⠑"]
+ - ["hunderace", "⠓⠥⠝⠹⠗⠁⠉⠑"]
+ - ["Dette", "⠠⠮⠳"]
+ - ["dette", "⠮⠳"]
+ - ["Detalje", "⠠⠹⠞⠁⠇⠚⠑"]
+ - ["detalje", "⠹⠞⠁⠇⠚⠑"]
+
+# Nocross multiple cells
+
+ - ["Endda", "⠠⠑⠟⠙⠁"]
+ - ["endda", "⠑⠟⠙⠁"]
+ - ["Morgendag", "⠠⠍⠭⠛⠣⠙⠁⠛"]
+ - ["morgendag", "⠍⠭⠛⠣⠙⠁⠛"]
+ - ["Gendanne", "⠠⠛⠣⠙⠁⠝⠫"]
+ - ["gendanne", "⠛⠣⠙⠁⠝⠫"]
+ - ["Generelt", "⠠⠻⠫⠷⠇⠞"]
+ - ["generelt", "⠻⠫⠷⠇⠞"]
+
+ - ["Fra!", "⠠⠋⠗⠁⠖"]
+ - ["fra!", "⠋⠗⠁⠖"]
+ - ["!Fra", "⠰⠖⠠⠖"]
+ - ["!fra", "⠖⠋⠗⠁"]
+ - ["'Af", "⠈⠠⠴"]
+ - ["'af", "⠈⠁⠋"]
+
+# No single cell contractions before or after dashes
+
+ - ["at-bliver", "⠁⠞⠤⠃"]
+ - ["d-d-du", "⠰⠙⠤⠰⠙⠤⠙⠥"]
+
+# Combinations with slashes and other punctuation signs
+
+ - ["at!", "⠁⠖"]
+ - ["bliver!", "⠃⠖"]
+ - ["og!", "⠉⠖"]
+ - ["Han/hun", "⠠⠽⠌⠥"]
+ - ["han/hun", "⠽⠌⠥"]
+ - ["Over/under", "⠠⠭⠌⠟"]
+ - ["over/under", "⠭⠌⠟"]
+ - ["Til/fra", "⠠⠞⠌⠖"]
+ - ["til/fra", "⠞⠌⠖"]
+
+# Combinations which require letsign
+
+ - ["1st", "⠼⠁⠰⠎⠞"]
+ - ["2nd", "⠼⠃⠰⠝⠙"]
+ - ["1A", "⠼⠁⠰⠠⠁"]
+ - ["1a", "⠼⠁⠰⠁"]
+ - ["2B", "⠼⠃⠰⠠⠃"]
+ - ["2b", "⠼⠃⠰⠃"]
+
+# Multi-pass te⠄sts
+
+ - ["~x |z", "⠘⠆⠰⠭ ⠘⠸⠰⠵"]
+ - ["~X |Z", "⠘⠆⠰⠠⠭ ⠘⠸⠰⠠⠵"]
+ - ["5É", "⠼⠑⠰⠠⠿"]
+
+# Examples from "Den danske punktskrift 2021"
+
+# Section 5.3.1 Syllables
+
+ - [Anders, ⠠⠁⠝⠾⠎]
+ - [banegården, ⠃⠁⠫⠛⠡⠗⠯]
+ - [banegårdene, ⠃⠁⠫⠛⠡⠗⠹⠫]
+ - [penge, ⠏⠣⠻]
+ - [ringe, ⠗⠊⠝⠻]
+ - [skriveregel, ⠿⠗⠊⠼⠷⠻⠇]
+ - [Roskilde, ⠠⠗⠕⠿⠊⠇⠹, {xfail: {forward: true}}]
+ - [hviske, ⠺⠊⠿⠑]
+ - [taske, ⠞⠁⠿⠑]
+ - [taste, ⠞⠁⠵⠑]
+ - [vristen, ⠧⠗⠊⠵⠣]
+
+# Section 5.3.3 Use last contraction
+
+ - [bager, ⠃⠁⠛⠱]
+ - [haner, ⠓⠁⠝⠱]
+
+# Section 5.3.4 "St" and "hv"
+
+ - [stedet, ⠵⠑⠮]
+ - [stene, ⠵⠑⠫]
+ - [hvede, ⠺⠑⠹]
+
+# Section 5.3.5 "Ve" as begword
+
+ - [vej, ⠧⠑⠚]
+ - [veje, ⠧⠑⠚⠑]
+ - [vejene, ⠼⠚⠑⠫]
+ - [veg, ⠧⠑⠛]
+ - [veda, ⠧⠑⠙⠁]
+
+# Section 5.3.6 Multiple cell partword
+
+ - [danskvand, ⠙⠿⠧⠁⠟]
+ - [danskhed, ⠙⠿⠓⠑⠙]
+ - [gangbro, ⠛⠛⠃⠗⠕]
+ - [gangbar, ⠛⠛⠃⠁⠗]
+ - [kvindelig, ⠅⠧⠹⠇⠔]
+ - [kvindekamp, ⠅⠧⠹⠅⠁⠍⠏]
+ - [menneskelig, ⠩⠿⠑⠇⠔]
+ - [problemformulering, ⠏⠃⠋⠭⠍⠥⠇⠑⠗⠊⠝⠛]
+ - [virkelighedsflugt, ⠧⠗⠅⠓⠑⠙⠎⠋⠇⠥⠛⠞]
+
+# Section 6.1 Period
+
+ - ["St. St. Blicher", "⠰⠠⠎⠞⠄ ⠰⠠⠎⠞⠄ ⠠⠃⠇⠊⠉⠓⠱"]
+ - ["Skt. Hans", "⠠⠿⠞⠄ ⠠⠽⠎"]
+
+# Section 6.4 Hyphens
+
+ - ["ret- og vrang-strikning", "⠗⠬⠤ ⠉ ⠧⠗⠁⠝⠛⠤⠵⠗⠊⠅⠝⠊⠝⠛"]
+ - [over-komme, ⠕⠧⠱⠤⠅⠕⠍⠩, {xfail: {forward: true}}]
+
+# Section 7.1 Capsletter
+
+ - - Han åbnede døren.
+ - ⠠⠽ ⠡⠃⠫⠹ ⠙⠪⠗⠣⠄
+ - - Jeg hilste på Søren og hans datter Xenia.
+ - ⠠⠚ ⠓⠊⠇⠵⠑ ⠏ ⠠⠎⠪⠗⠣ ⠉ ⠽⠎ ⠙⠁⠞⠞⠱ ⠰⠠⠭⠑⠝⠊⠁⠄
+ - - Jeg læser gerne bøger af H. C. Andersen og St. St. Blicher.
+ - ⠠⠚ ⠇⠜⠎⠱ ⠛⠱⠫ ⠃⠪⠛⠱ ⠴ ⠰⠠⠓⠄ ⠰⠠⠉⠄ ⠠⠁⠝⠾⠎⠣ ⠉ ⠰⠠⠎⠞⠄ ⠰⠠⠎⠞⠄ ⠠⠃⠇⠊⠉⠓⠱⠄
+ - - Læs afsnit A underafsnit a
+ - ⠠⠇⠜⠎ ⠁⠋⠎⠝⠊⠞ ⠰⠠⠁ ⠥⠝⠾⠁⠋⠎⠝⠊⠞ ⠰⠁
+
+# Section 7.2 Capsword
+
+ - [DBS, ⠸⠙⠃⠎]
+ - ["DEN GAMLE MAND OG HAVET", "⠸⠯ ⠸⠛⠁⠍⠇⠑ ⠸⠍⠁⠟ ⠸⠉ ⠸⠓⠁⠧⠬"]
+ - ["Din kode er DSB345Qz", "⠠⠙⠝ ⠅⠕⠹ ⠱ ⠸⠙⠎⠃⠰⠼⠉⠙⠑⠰⠠⠟⠰⠵"]
+ - ["NATOs", "⠸⠝⠁⠞⠕⠰⠎"]
+ - ["DBS's forretningsudvalg.", "⠸⠙⠃⠎⠈⠰⠎ ⠋⠭⠗⠬⠝⠊⠝⠛⠎⠥⠙⠧⠁⠇⠛⠄"]
+ - ["TiBS", "⠠⠞⠊⠸⠃⠎"]
+
+# Section 7.3 Letsign
+
+ - - Skjern Å blev rettet ud.
+ - ⠠⠿⠚⠱⠝ ⠰⠠⠡ ⠃⠧ ⠗⠬⠞⠬ ⠥⠙⠄
+ - [jazz, ⠚⠁⠰⠵⠰⠵]
+ - - han løb fx 2 km.
+ - ⠽ ⠇⠪⠃ ⠋⠰⠭ ⠼⠃ ⠰⠅⠍⠄
+ - - han fik 5 kr. (1%). Sidste år var det mindre (1‰).
+ - ⠽ ⠋⠅ ⠼⠑ ⠅⠗⠄ ⠦⠼⠁ ⠚⠴⠰⠴⠄ ⠠⠎⠵⠑ ⠡⠗ ⠼ ⠮ ⠍⠊⠝⠙⠷ ⠦⠼⠁ ⠚⠴⠴⠰⠴⠄
+
+# Section 7.4 Emphasis
+
+flags: {testmode: forward}
+tests:
+ - - Du spørger, hvad jeg laver. Hvad laver du?
+ - ⠠⠙ ⠎⠏⠪⠗⠛⠱⠂ ⠺ ⠨⠚⠨ ⠇⠁⠧⠱⠄ ⠠⠺ ⠇⠁⠧⠱ ⠨⠙⠨⠢
+ - typeform:
+ italic: " +++ ++ "
+ - - Hvad du gør nu, lige nu i dette øjeblik, får betydning for dig resten af dit liv.
+ - ⠠⠺ ⠙ ⠛ ⠝⠥⠂ ⠨⠇ ⠝⠥ ⠊ ⠮⠳ ⠪⠚⠃⠂⠨ ⠋⠡⠗ ⠃⠑⠞⠽⠙⠝⠊⠝⠛ ⠋ ⠨⠙⠔⠨ ⠷⠵⠣ ⠴ ⠙⠞ ⠇⠊⠧⠄
+ - typeform:
+ italic: " ++++++++++++++++++++++++ +++ "
+ - - Den gamle stavemåde var Revshaleøen.
+ - ⠠⠯ ⠛⠁⠍⠇⠑ ⠵⠁⠼⠍⠡⠹ ⠼ ⠠⠷⠨⠧⠨⠎⠓⠁⠇⠑⠪⠣⠄
+ - typeform:
+ italic: " + "
+
+# Section 7.5.2 Misc letters with accents
+
+flags: {testmode: bothDirections}
+tests:
+ - ["Antonín Dvořák", "⠠⠁⠝⠞⠕⠝⠐⠊⠝ ⠠⠙⠧⠕⠐⠗⠐⠁⠅"]
+ - [Lübeck, ⠠⠇⠰⠳⠃⠑⠉⠅]
+ - [Göteborg, ⠠⠛⠐⠪⠳⠃⠭⠛]
+ - [Malmö, ⠠⠍⠁⠇⠍⠐⠪]
+ - [Gävle, ⠠⠛⠐⠜⠧⠇⠑]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+# Single characters
+
+ - ["§", "⠬"]
+
+# Parentheses and misc
+
+ - ["J) j) %) ') \u2030) \u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠰⠠⠚⠰⠴ ⠰⠚⠰⠴ ⠚⠴⠰⠴ ⠈⠰⠴ ⠚⠴⠴⠰⠴ ⠚⠴⠴⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴ ⠈⠰⠴"]
+
+# Emphasis
+# Single letter emphasis currently fails due to contraction across emphasis.
+
+ - # Bold line
+ - En linje med fed.
+ - ⠨⠠⠣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠠⠬ ⠨⠭⠙⠨ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠠⠬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - {typeform: {bold: ' + '}, xfail: true}
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠨⠠⠣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠨
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠠⠬ ⠨⠭⠙⠨ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠠⠬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - {typeform: {italic: ' + '}, xfail: true}
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠨⠠⠣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄⠨
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠠⠬ ⠨⠭⠙⠨ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠠⠬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - {typeform: {underline: ' + '}, xfail: true}
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠨⠠⠣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠠⠬ ⠨⠭⠙⠨ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠠⠬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - {typeform: {bold: ' + ', italic: ' + ', underline: ' + '}, xfail: true}
+
diff --git a/LibLouis.NET.Test/braille-specs/da-dk-8dot.yaml b/LibLouis.NET.Test/braille-specs/da-dk-8dot.yaml
new file mode 100644
index 0000000..4c0e058
--- /dev/null
+++ b/LibLouis.NET.Test/braille-specs/da-dk-8dot.yaml
@@ -0,0 +1,1003 @@
+display: unicode-without-blank.dis
+
+# ----------------
+# Grade 0, 1 and 2
+# ----------------
+
+table: {language: da, grade: 0, dots: 8, version: 2022, __assert-match: da-dk-g08.ctb}
+table: {language: da, grade: 1, dots: 8, version: 2022, __assert-match: da-dk-g18.ctb}
+table: {language: da, grade: 2, dots: 8, version: 2022, __assert-match: da-dk-g28.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - [' ', ' ']
+ - ['"', '⠶']
+ - ['$', '⣙']
+ - ['%', '⣠']
+ - ['&', '⢯']
+ - ['''', '⠈']
+ - ['(', '⢦']
+ - [')', '⢴']
+ - ['+', '⢖']
+ - [',', '⠂']
+ - ['-', '⠤']
+ - ['.', '⠄']
+ - ['/', '⢌']
+ - ['0', '⢚']
+ - ['1', '⢁']
+ - ['2', '⢃']
+ - ['3', '⢉']
+ - ['4', '⢙']
+ - ['5', '⢑']
+ - ['6', '⢋']
+ - ['7', '⢛']
+ - ['8', '⢓']
+ - ['9', '⢊']
+ - [':', '⠒']
+ - [';', '⠆']
+ - ['<', '⢔']
+ - ['=', '⢶']
+ - ['>', '⡢']
+ - ['?', '⠢']
+ - ['@', '⣁']
+ - ['[', '⣦']
+ - [']', '⣴']
+ - ['^', '⢏']
+ - ['_', '⣤']
+ - ['`', '⠐']
+ - ['{', '⣧']
+ - ['|', '⢸']
+ - ['}', '⣼']
+ - ['~', '⣆']
+ - ['€', '⣑']
+ - ['‚', '⡘']
+ - ['ƒ', '⢐']
+ - ['„', '⡨']
+ - ['†', '⠘']
+ - ['‡', '⠸']
+ - ['ˆ', '⣰']
+ - ['‰', '⣺']
+ - ['‹', '⣖']
+ - ['‘', '⡈']
+ - ['’', '⢈']
+ - ['“', '⡆']
+ - ['”', '⢰']
+ - ['•', '⡄']
+ - ['–', '⢤']
+ - ['—', '⡤']
+ - ['˜', '⣎']
+ - ['™', '⣞']
+ - ['›', '⡸']
+ - ['¢', '⣒']
+ - ['£', '⣇']
+ - ['¥', '⣽']
+ - ['¦', '⣌']
+ - ['§', '⣐']
+ - ['¨', '⠰']
+ - ['©', '⣉']
+ - ['«', '⡐']
+ - ['®', '⣗']
+ - ['¯', '⡶']
+ - ['±', '⣋']
+ - ['²', '⢆']
+ - ['³', '⢒']
+ - ['´', '⢘']
+ - ['¶', '⢿']
+ - ['·', '⢄']
+ - ['¸', '⣨']
+ - ['¹', '⢂']
+ - ['»', '⡰']
+ - ['¼', '⢥']
+ - ['½', '⢨']
+ - ['¾', '⢭']
+ - ['÷', '⢲']
+
+# -------------
+# Grade 0 and 1
+# -------------
+
+table: {language: da, grade: 0, dots: 8, version: 2022}
+table: {language: da, grade: 1, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - ['*', '⠔']
+ - ['A', '⡁']
+ - ['B', '⡃']
+ - ['C', '⡉']
+ - ['D', '⡙']
+ - ['E', '⡑']
+ - ['F', '⡋']
+ - ['G', '⡛']
+ - ['H', '⡓']
+ - ['I', '⡊']
+ - ['J', '⡚']
+ - ['K', '⡅']
+ - ['L', '⡇']
+ - ['M', '⡍']
+ - ['N', '⡝']
+ - ['O', '⡕']
+ - ['P', '⡏']
+ - ['Q', '⡟']
+ - ['R', '⡗']
+ - ['S', '⡎']
+ - ['T', '⡞']
+ - ['U', '⡥']
+ - ['V', '⡧']
+ - ['W', '⡺']
+ - ['X', '⡭']
+ - ['Y', '⡽']
+ - ['Z', '⡵']
+ - ['\\', '⡌']
+ - ['a', '⠁']
+ - ['b', '⠃']
+ - ['c', '⠉']
+ - ['d', '⠙']
+ - ['e', '⠑']
+ - ['f', '⠋']
+ - ['g', '⠛']
+ - ['h', '⠓']
+ - ['i', '⠊']
+ - ['j', '⠚']
+ - ['k', '⠅']
+ - ['l', '⠇']
+ - ['m', '⠍']
+ - ['n', '⠝']
+ - ['o', '⠕']
+ - ['p', '⠏']
+ - ['q', '⠟']
+ - ['r', '⠗']
+ - ['s', '⠎']
+ - ['t', '⠞']
+ - ['u', '⠥']
+ - ['v', '⠧']
+ - ['w', '⠺']
+ - ['x', '⠭']
+ - ['y', '⠽']
+ - ['z', '⠵']
+ - ['…', '⠠']
+ - ['Œ', '⣕']
+ - ['Ž', '⡬']
+ - ['œ', '⢕']
+ - ['ž', '⠬']
+ - ['Ÿ', '⣾']
+ - ['¡', '⠲']
+ - ['¤', '⡦']
+ - ['ª', '⣮']
+ - ['°', '⠴']
+ - ['µ', '⠦']
+ - ['º', '⣿']
+ - ['À', '⡷']
+ - ['Á', '⣷']
+ - ['Â', '⣡']
+ - ['Ã', '⣩']
+ - ['Ä', '⣜']
+ - ['Å', '⡡']
+ - ['Æ', '⡜']
+ - ['Ç', '⡯']
+ - ['È', '⡮']
+ - ['É', '⡿']
+ - ['Ê', '⡣']
+ - ['Ë', '⡫']
+ - ['Ì', '⣱']
+ - ['Í', '⣣']
+ - ['Î', '⡩']
+ - ['Ï', '⡻']
+ - ['Ð', '⡠']
+ - ['Ñ', '⣻']
+ - ['Ò', '⣫']
+ - ['Ó', '⣬']
+ - ['Ô', '⡹']
+ - ['Õ', '⣹']
+ - ['Ö', '⣪']
+ - ['×', '⠼']
+ - ['Ø', '⡪']
+ - ['Ù', '⡾']
+ - ['Ú', '⣳']
+ - ['Û', '⡱']
+ - ['Ü', '⡳']
+ - ['Ý', '⣶']
+ - ['Þ', '⣅']
+ - ['ß', '⢮']
+ - ['à', '⠷']
+ - ['á', '⢷']
+ - ['â', '⢡']
+ - ['ã', '⢩']
+ - ['ä', '⢜']
+ - ['å', '⠡']
+ - ['æ', '⠜']
+ - ['ç', '⠯']
+ - ['è', '⠮']
+ - ['é', '⠿']
+ - ['ê', '⠣']
+ - ['ë', '⠫']
+ - ['ì', '⢱']
+ - ['í', '⢣']
+ - ['î', '⠩']
+ - ['ï', '⠻']
+ - ['ð', '⢽']
+ - ['ñ', '⢻']
+ - ['ò', '⢫']
+ - ['ó', '⢬']
+ - ['ô', '⠹']
+ - ['õ', '⢹']
+ - ['ö', '⢪']
+ - ['ø', '⠪']
+ - ['ù', '⠾']
+ - ['ú', '⢳']
+ - ['û', '⠱']
+ - ['ü', '⠳']
+ - ['ý', '⢍']
+ - ['þ', '⢅']
+ - ['ÿ', '⢾']
+
+# Pangram
+
+ - - 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon.'
+ - '⡟⠥⠊⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍⠑⠙ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⡺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏⠡ ⠭⠽⠇⠕⠋⠕⠝⠄'
+
+# -------
+# Grade 0
+# -------
+
+table: {language: da, grade: 0, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+ - ['\x0009', '⣊']
+ - ['\x000a', '⣚']
+ - ['\x000b', '⢝']
+ - ['\x000c', '⢇']
+ - ['\x000d', '⡒']
+
+# Characters from 0x21 to 0xff
+
+ - ['Š', '⠨']
+ - ['š', '⢎']
+ - [' ', '⢞']
+
+# -------------
+# Grade 1 and 2
+# -------------
+
+table: {language: da, grade: 1, dots: 8, version: 2022}
+table: {language: da, grade: 2, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Misc Unicode chars
+# For each accented letter, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ['\x0100\x0101', '⠐⡁⠐⠁']
+ - ['\x0106\x0107', '⠐⡉⠐⠉']
+ - ['\x010e\x010f', '⠐⡙⠐⠙']
+ - ['\x0112\x0113', '⠐⡑⠐⠑']
+ - ['\x011c\x011d', '⠐⡛⠐⠛']
+ - ['\x0124\x0125', '⠐⡓⠐⠓']
+ - ['\x0128\x0129', '⠐⡊⠐⠊']
+ - ['\x0134\x0135', '⠐⡚⠐⠚']
+ - ['\x0136\x0137', '⠐⡅⠐⠅']
+ - ['\x0138', '⠐⠟']
+ - ['\x0139\x013a', '⠐⡇⠐⠇']
+ - ['\x0143\x0144', '⠐⡝⠐⠝']
+ - ['\x014c\x014d', '⠐⡕⠐⠕']
+ - ['\x0154\x0155', '⠐⡗⠐⠗']
+ - ['\x0162\x0163', '⠐⡞⠐⠞']
+ - ['\x0168\x0169', '⠐⡥⠐⠥']
+ - ['\x0174\x0175', '⠐⡺⠐⠺']
+ - ['\x0176\x0177', '⠐⡽⠐⠽']
+ - ['\x0179\x017a', '⠐⡵⠐⠵']
+
+# Punctuation and bullits
+
+ - ['\x2016', '⠘⢸']
+ - ['\x2017', '⠘⣤']
+
+# Arrows
+
+ - ['\x2190', '⠘⠳⠪']
+ - ['\x2191', '⠘⠳⠬']
+ - ['\x2192', '⠘⠳⠕']
+ - ['\x2193', '⠘⠳⠩']
+ - ['\x2194', '⠘⠳⠺⠗⠕']
+ - ['\x2196', '⠘⠳⠱']
+ - ['\x2197', '⠘⠳⠎']
+ - ['\x2198', '⠘⠳⠣']
+ - ['\x2199', '⠘⠳⠜']
+ - ['\x21D4', '⠘⠳⠺⠶⠗⠕']
+
+# Math signs (experimental)
+
+ - ['\x2200', '⠘⠁']
+ - ['\x2208', '⠘⠑']
+ - ['\x2213', '⠸⠤']
+ - ['\x221d', '⠸⠐⢶']
+ - ['\x2229', '⠨⠦']
+ - ['\x222a', '⠨⠖']
+ - ['\x2243', '⠸⠔']
+ - ['\x2245', '⠐⠘⠔']
+ - ['\x2248', '⠘⠔']
+ - ['\x224f', '⠘⠐⢶']
+ - ['\x2251', '⠨⠐⢶']
+ - ['\x2260', '⠐⢶⠈⠱']
+ - ['\x2261', '⠸⠿']
+ - ['\x2264', '⠸⢔']
+ - ['\x2265', '⠸⡢']
+ - ['\x226a', '⠨⢔']
+ - ['\x226b', '⠨⡢']
+ - ['\x22c5', '⠐⠲']
+
+# Section sign
+
+ - ["§ 3", "⣐⢉"]
+ - ["§ 3:", "⣐⢉⠒"]
+ - ["§§ 3-5", "⣐⣐⢉⠤⢑"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+ - ['\x0009', ' ']
+ - ['\x000a', ' ']
+ - ['\x000b', ' ']
+ - ['\x000c', ' ']
+ - ['\x000d', ' ']
+ - ['\x00a0', ' ']
+ - ['\x0102\x0103', '⠐⡁⠐⠁']
+ - ['\x0104\x0105', '⠐⡁⠐⠁']
+ - ['\x0108\x0109', '⠐⡉⠐⠉']
+ - ['\x010a\x010b', '⠐⡉⠐⠉']
+ - ['\x010c\x010d', '⠐⡉⠐⠉']
+ - ['\x0110\x0111', '⠐⡙⠐⠙']
+ - ['\x0114\x0115', '⠐⡑⠐⠑']
+ - ['\x0116\x0117', '⠐⡑⠐⠑']
+ - ['\x0118\x0119', '⠐⡑⠐⠑']
+ - ['\x011a\x011b', '⠐⡑⠐⠑']
+ - ['\x011e\x011f', '⠐⡛⠐⠛']
+ - ['\x0120\x0121', '⠐⡛⠐⠛']
+ - ['\x0122\x0123', '⠐⡛⠐⠛']
+ - ['\x0126\x0127', '⠐⡓⠐⠓']
+ - ['\x012a\x012b', '⠐⡊⠐⠊']
+ - ['\x012c\x012d', '⠐⡊⠐⠊']
+ - ['\x012e\x012f', '⠐⡊⠐⠊']
+ - ['\x0130\x0131', '⠐⡊⠐⠊']
+ - ['\x0132\x0133', '⡊⠚⠊⠚']
+ - ['\x013b\x013c', '⠐⡇⠐⠇']
+ - ['\x013d\x013e', '⠐⡇⠐⠇']
+ - ['\x013f\x0140', '⠐⡇⠐⠇']
+ - ['\x0141\x0142', '⠐⡇⠐⠇']
+ - ['\x0145\x0146', '⠐⡝⠐⠝']
+ - ['\x0147\x0148', '⠐⡝⠐⠝']
+ - ['\x0149', '⠈⠝']
+ - ['\x014a\x014b', '⠐⡝⠐⠝']
+ - ['\x014e\x014f', '⠐⡕⠐⠕']
+ - ['\x0150\x0151', '⠐⡕⠐⠕']
+ - ['\x0156\x0157', '⠐⡗⠐⠗']
+ - ['\x0158\x0159', '⠐⡗⠐⠗']
+ - ['\x015a\x015b', '⠐⡎⠐⠎']
+ - ['\x015c\x015d', '⠐⡎⠐⠎']
+ - ['\x015e\x015f', '⠐⡎⠐⠎']
+ - ['\x0164\x0165', '⠐⡞⠐⠞']
+ - ['\x0166\x0167', '⠐⡞⠐⠞']
+ - ['\x016a\x016b', '⠐⡥⠐⠥']
+ - ['\x016c\x016d', '⠐⡥⠐⠥']
+ - ['\x016e\x016f', '⠐⡥⠐⠥']
+ - ['\x0170\x0171', '⠐⡥⠐⠥']
+ - ['\x0172\x0173', '⠐⡥⠐⠥']
+ - ['\x017b\x017c', '⠐⡵⠐⠵']
+ - ['\x017f', '⠐⠎']
+
+# Punctuation and bullits
+
+ - ['\x2000', ' ']
+ - ['\x2001', ' ']
+ - ['\x2002', ' ']
+ - ['\x2003', ' ']
+ - ['\x2004', ' ']
+ - ['\x2005', ' ']
+ - ['\x2006', ' ']
+ - ['\x2007', ' ']
+ - ['\x2008', ' ']
+ - ['\x2009', ' ']
+ - ['\x200a', ' ']
+ - ['\x2010', '⠤']
+ - ['\x2011', '⠤']
+ - ['\x2012', '⠤']
+ - ['\x201b', '⠈']
+ - ['\x201f', '⠶']
+ - ['\x2023', '⡄']
+ - ['\x202f', ' ']
+ - ['\x203c', '⠖⠖']
+ - ['\x203d', '⠢⠖']
+ - ['\x2043', '⡄']
+ - ['\x2047', '⠢⠢']
+ - ['\x2048', '⠢⠖']
+ - ['\x2049', '⠖⠢']
+ - ['\x204c', '⡄']
+ - ['\x204d', '⡄']
+
+# Geometrical shapes
+
+ - ['\x25e6', '⡄']
+
+# Tests that apply for grade 1 and 2 but currently fail for grade 2
+
+table: {language: da, grade: 1, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Greek letters (with dots 458 as prefix)
+
+ - ['\x0386\x03ac', '⢘⠐⡁⢘⠐⠁']
+ - ['\x0388\x03ad', '⢘⠐⡑⢘⠐⠑']
+ - ['\x0389\x03ae', '⢘⠐⡱⢘⠐⠱']
+ - ['\x038a\x03af', '⢘⠐⡊⢘⠐⠊']
+ - ['\x038c\x03cc', '⢘⠐⡕⢘⠐⠕']
+ - ['\x038e\x03cd', '⢘⠐⡥⢘⠐⠥']
+ - ['\x038f\x03ce', '⢘⠐⡺⢘⠐⠺']
+ - ['\x0391\x03b1', '⢘⡁⢘⠁']
+ - ['\x0392\x03b2', '⢘⡃⢘⠃']
+ - ['\x0393\x03b3', '⢘⡛⢘⠛']
+ - ['\x0394\x03b4', '⢘⡙⢘⠙']
+ - ['\x0395\x03b5', '⢘⡑⢘⠑']
+ - ['\x0396\x03b6', '⢘⡵⢘⠵']
+ - ['\x0397\x03b7', '⢘⡱⢘⠱']
+ - ['\x0398\x03b8', '⢘⡹⢘⠹']
+ - ['\x0399\x03b9', '⢘⡊⢘⠊']
+ - ['\x039a\x03ba', '⢘⡅⢘⠅']
+ - ['\x039b\x03bb', '⢘⡇⢘⠇']
+ - ['\x039c\x03bc', '⢘⡍⢘⠍']
+ - ['\x039d\x03bd', '⢘⡝⢘⠝']
+ - ['\x039e\x03be', '⢘⡭⢘⠭']
+ - ['\x039f\x03bf', '⢘⡕⢘⠕']
+ - ['\x03a0\x03c0', '⢘⡏⢘⠏']
+ - ['\x03a1\x03c1', '⢘⡗⢘⠗']
+ - ['\x03a3\x03c3', '⢘⡎⢘⠎']
+ - ['\x03a4\x03c4', '⢘⡞⢘⠞']
+ - ['\x03a5\x03c5', '⢘⡥⢘⠥']
+ - ['\x03a6\x03c6', '⢘⡋⢘⠋']
+ - ['\x03a7\x03c7', '⢘⡯⢘⠯']
+ - ['\x03a8\x03c8', '⢘⡽⢘⠽']
+ - ['\x03a9\x03c9', '⢘⡺⢘⠺']
+
+# same tests but with xfail
+table: {language: da, grade: 2, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+ - ['\x0386\x03ac', '⢘⠐⡁⢘⠐⠁', {xfail: {forward: true}}]
+ - ['\x0388\x03ad', '⢘⠐⡑⢘⠐⠑', {xfail: {forward: true}}]
+ - ['\x0389\x03ae', '⢘⠐⡱⢘⠐⠱', {xfail: {forward: true}}]
+ - ['\x038a\x03af', '⢘⠐⡊⢘⠐⠊', {xfail: {forward: true}}]
+ - ['\x038c\x03cc', '⢘⠐⡕⢘⠐⠕', {xfail: {forward: true}}]
+ - ['\x038e\x03cd', '⢘⠐⡥⢘⠐⠥', {xfail: {forward: true}}]
+ - ['\x038f\x03ce', '⢘⠐⡺⢘⠐⠺', {xfail: {forward: true}}]
+ - ['\x0391\x03b1', '⢘⡁⢘⠁', {xfail: {forward: true}}]
+ - ['\x0392\x03b2', '⢘⡃⢘⠃', {xfail: {forward: true}}]
+ - ['\x0393\x03b3', '⢘⡛⢘⠛', {xfail: {forward: true}}]
+ - ['\x0394\x03b4', '⢘⡙⢘⠙', {xfail: {forward: true}}]
+ - ['\x0395\x03b5', '⢘⡑⢘⠑', {xfail: {forward: true}}]
+ - ['\x0396\x03b6', '⢘⡵⢘⠵', {xfail: {forward: true}}]
+ - ['\x0397\x03b7', '⢘⡱⢘⠱', {xfail: {forward: true}}]
+ - ['\x0398\x03b8', '⢘⡹⢘⠹', {xfail: {forward: true}}]
+ - ['\x0399\x03b9', '⢘⡊⢘⠊', {xfail: {forward: true}}]
+ - ['\x039a\x03ba', '⢘⡅⢘⠅', {xfail: {forward: true}}]
+ - ['\x039b\x03bb', '⢘⡇⢘⠇', {xfail: {forward: true}}]
+ - ['\x039c\x03bc', '⢘⡍⢘⠍', {xfail: {forward: true}}]
+ - ['\x039d\x03bd', '⢘⡝⢘⠝', {xfail: {forward: true}}]
+ - ['\x039e\x03be', '⢘⡭⢘⠭', {xfail: {forward: true}}]
+ - ['\x039f\x03bf', '⢘⡕⢘⠕', {xfail: {forward: true}}]
+ - ['\x03a0\x03c0', '⢘⡏⢘⠏', {xfail: {forward: true}}]
+ - ['\x03a1\x03c1', '⢘⡗⢘⠗', {xfail: {forward: true}}]
+ - ['\x03a3\x03c3', '⢘⡎⢘⠎', {xfail: {forward: true}}]
+ - ['\x03a4\x03c4', '⢘⡞⢘⠞', {xfail: {forward: true}}]
+ - ['\x03a5\x03c5', '⢘⡥⢘⠥', {xfail: {forward: true}}]
+ - ['\x03a6\x03c6', '⢘⡋⢘⠋', {xfail: {forward: true}}]
+ - ['\x03a7\x03c7', '⢘⡯⢘⠯', {xfail: {forward: true}}]
+ - ['\x03a8\x03c8', '⢘⡽⢘⠽', {xfail: {forward: true}}]
+ - ['\x03a9\x03c9', '⢘⡺⢘⠺', {xfail: {forward: true}}]
+
+# -------
+# Grade 1
+# -------
+
+table: {language: da, grade: 1, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - ['Š', '⠐⡎']
+ - ['š', '⠐⠎']
+
+# Emphasis (cannot be back-translated)
+
+flags: {testmode: forward}
+tests:
+
+ - # Bold line
+ - En linje med fed.
+ - ⠨⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠋⠑⠙⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⡑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⡑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠨⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄⠨
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⡑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⡑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠨⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠨
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⡑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⡑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠨⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠁⠇⠞⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⡑⠞ ⠨⠕⠗⠙⠨ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⡑⠞ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# -------
+# Grade 2
+# -------
+
+table: {language: da, grade: 2, dots: 8, version: 2022}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - ['*', '⠰⠔']
+ - ['A', '⠰⡁']
+ - ['B', '⠰⡃']
+ - ['C', '⠰⡉']
+ - ['D', '⠰⡙']
+ - ['E', '⠰⡑']
+ - ['F', '⠰⡋']
+ - ['G', '⠰⡛']
+ - ['H', '⠰⡓']
+ - ['I', '⡊']
+ - ['J', '⠰⡚']
+ - ['K', '⠰⡅']
+ - ['L', '⠰⡇']
+ - ['M', '⠰⡍']
+ - ['N', '⠰⡝']
+ - ['O', '⠰⡕']
+ - ['P', '⠰⡏']
+ - ['Q', '⠰⡟']
+ - ['R', '⠰⡗']
+ - ['S', '⠰⡎']
+ - ['T', '⠰⡞']
+ - ['U', '⠰⡥']
+ - ['V', '⠰⡧']
+ - ['W', '⠰⡺']
+ - ['X', '⠰⡭']
+ - ['Y', '⠰⡽']
+ - ['Z', '⠰⡵']
+ - ['\\', '⠰⡌']
+ - ['a', '⠰⠁']
+ - ['b', '⠰⠃']
+ - ['c', '⠰⠉']
+ - ['d', '⠰⠙']
+ - ['e', '⠰⠑']
+ - ['f', '⠰⠋']
+ - ['g', '⠰⠛']
+ - ['h', '⠰⠓']
+ - ['i', '⠊']
+ - ['j', '⠰⠚']
+ - ['k', '⠰⠅']
+ - ['l', '⠰⠇']
+ - ['m', '⠰⠍']
+ - ['n', '⠰⠝']
+ - ['o', '⠰⠕']
+ - ['p', '⠰⠏']
+ - ['q', '⠰⠟']
+ - ['r', '⠰⠗']
+ - ['s', '⠰⠎']
+ - ['t', '⠰⠞']
+ - ['u', '⠰⠥']
+ - ['v', '⠰⠧']
+ - ['w', '⠰⠺']
+ - ['x', '⠰⠭']
+ - ['y', '⠰⠽']
+ - ['z', '⠰⠵']
+ - ['…', '⠰⠄⠄⠄']
+ - ['Š', '⠐⡎']
+ - ['Œ', '⠰⣕']
+ - ['Ž', '⠰⡬']
+ - ['š', '⠐⠎']
+ - ['œ', '⠰⢕']
+ - ['ž', '⠰⠬']
+ - ['Ÿ', '⠰⣾']
+ - ['¡', '⠰⠲']
+ - ['¤', '⠰⡦']
+ - ['ª', '⠰⣮']
+ - ['¬', '⠰⡼']
+ - ['', '⠰⣄']
+ - ['°', '⠰⠴']
+ - ['µ', '⠰⠦']
+ - ['º', '⠰⣿']
+ - ['À', '⠰⡷']
+ - ['Á', '⠰⣷']
+ - ['Â', '⠰⣡']
+ - ['Ã', '⠰⣩']
+ - ['Ä', '⠰⣜']
+ - ['Å', '⠰⡡']
+ - ['Æ', '⠰⡜']
+ - ['Ç', '⠰⡯']
+ - ['È', '⠰⡮']
+ - ['É', '⠰⡿']
+ - ['Ê', '⠰⡣']
+ - ['Ë', '⠰⡫']
+ - ['Ì', '⠰⣱']
+ - ['Í', '⠰⣣']
+ - ['Î', '⠰⡩']
+ - ['Ï', '⠰⡻']
+ - ['Ð', '⠰⡠']
+ - ['Ñ', '⠰⣻']
+ - ['Ò', '⠰⣫']
+ - ['Ó', '⠰⣬']
+ - ['Ô', '⠰⡹']
+ - ['Õ', '⠰⣹']
+ - ['Ö', '⠰⣪']
+ - ['×', '⠰⠼']
+ - ['Ø', '⠰⡪']
+ - ['Ù', '⠰⡾']
+ - ['Ú', '⠰⣳']
+ - ['Û', '⠰⡱']
+ - ['Ü', '⠰⡳']
+ - ['Ý', '⠰⣍']
+ - ['Þ', '⠰⣅']
+ - ['ß', '⠰⢮']
+ - ['à', '⠰⠷']
+ - ['á', '⠰⢷']
+ - ['â', '⠰⢡']
+ - ['ã', '⠰⢩']
+ - ['ä', '⠰⢜']
+ - ['å', '⠰⠡']
+ - ['æ', '⠰⠜']
+ - ['ç', '⠰⠯']
+ - ['è', '⠰⠮']
+ - ['é', '⠰⠿']
+ - ['ê', '⠰⠣']
+ - ['ë', '⠰⠫']
+ - ['ì', '⠰⢱']
+ - ['í', '⠰⢣']
+ - ['î', '⠰⠩']
+ - ['ï', '⠰⠻']
+ - ['ð', '⠰⢽']
+ - ['ñ', '⠰⢻']
+ - ['ò', '⠰⢫']
+ - ['ó', '⠰⢬']
+ - ['ô', '⠰⠹']
+ - ['õ', '⠰⢹']
+ - ['ö', '⠰⢪']
+ - ['ø', '⠰⠪']
+ - ['ù', '⠰⠾']
+ - ['ú', '⠰⢳']
+ - ['û', '⠰⠱']
+ - ['ü', '⠰⠳']
+ - ['ý', '⠰⢍']
+ - ['þ', '⠰⢅']
+ - ['ÿ', '⠰⢾']
+
+# Pangram
+
+ - - 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon.'
+ - '⠰⡟⠥⠊⠰⠵⠹⠇⠞⠁⠛⠱⠫ ⠎⠏⠊⠵⠑ ⠚⠭⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠹⠂ ⠍⠣⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠣ ⠰⡺⠁⠇⠞⠓⠱ ⠎⠏⠊⠇⠇⠑⠹ ⠏ ⠰⠭⠽⠇⠕⠋⠕⠝⠄'
+
+# Inverted exclamation
+
+ - ['¡Que lastima!', '⠰⠲⠰⡟⠥⠑ ⠇⠁⠵⠊⠍⠁⠖']
+
+# No letsign before numbers
+
+ - ['v8', '⠰⠧⢓']
+ - ['A4', '⠰⡁⢙']
+ - ['Eleva2ren', '⡑⠇⠑⠧⠁⢃⠗⠣']
+
+# URLs emails and file names
+
+ - ['$at', '⣙⠁⠞']
+ - ['\\at\\bliver', '⠰⡌⠁⠞⠰⡌⠃⠇⠊⠧⠑⠗']
+ - ['at@bliver.og', '⠁⠞⣁⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['http://at.bliver.og', '⠓⠞⠞⠏⠒⢌⢌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['www.at.bliver.og', '⠺⠺⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['www.at.bliver.com', '⠺⠺⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍']
+ - ['www.a.b.c', '⠺⠺⠺⠄⠰⠁⠄⠰⠃⠄⠰⠉']
+
+# Word contractions
+
+ - ['At', '⡁']
+ - ['at', '⠁']
+ - ['Aldrig', '⡁⠔']
+ - ['aldrig', '⠁⠔']
+ - ['aig', '⠁⠊⠛']
+ - ['Alle', '⡁⠑']
+ - ['alle', '⠁⠑']
+ - ['ae', '⠰⠁⠑']
+ - ['Allerede', '⡁⠇⠗']
+ - ['allerede', '⠁⠇⠗']
+ - ['alr', '⠰⠁⠇⠗']
+ - ['Alligevel', '⡁⠇⠧']
+ - ['alligevel', '⠁⠇⠧']
+ - ['alv', '⠰⠁⠇⠧']
+ - ['Altid', '⡁⠞⠙']
+ - ['altid', '⠁⠞⠙']
+ - ['atd', '⠰⠁⠞⠙']
+ - ['Altså', '⡁⠡']
+ - ['altså', '⠁⠡']
+ - ['aå', '⠰⠁⠡']
+ - ['Bliver', '⡃']
+ - ['bliver', '⠃']
+ - ['Og', '⡉']
+ - ['og', '⠉']
+ - ['Deres', '⡲']
+ - ['deres', '⠲']
+ - ['Du', '⡙']
+ - ['du', '⠙']
+ - ['Eller', '⡑']
+ - ['eller', '⠑']
+ - ['For', '⡋']
+ - ['for', '⠋']
+ - ['Gør', '⡛']
+ - ['gør', '⠛']
+ - ['Har', '⡓']
+ - ['har', '⠓']
+ - ['Jeg', '⡚']
+ - ['jeg', '⠚']
+ - ['Kan', '⡅']
+ - ['kan', '⠅']
+ - ['Lige', '⡇']
+ - ['lige', '⠇']
+ - ['Med', '⡍']
+ - ['med', '⠍']
+ - ['Når', '⡝']
+ - ['når', '⠝']
+ - ['Op', '⡕']
+ - ['op', '⠕']
+ - ['På', '⡏']
+ - ['på', '⠏']
+ - ['Under', '⡟']
+ - ['under', '⠟']
+ - ['Rigtig', '⡗']
+ - ['rigtig', '⠗']
+ - ['Som', '⡎']
+ - ['som', '⠎']
+ - ['Til', '⡞']
+ - ['til', '⠞']
+ - ['Hun', '⡥']
+ - ['hun', '⠥']
+ - ['Ved', '⡧']
+ - ['ved', '⠧']
+ - ['Hvad', '⡺']
+ - ['hvad', '⠺']
+ - ['Over', '⡭']
+ - ['over', '⠭']
+ - ['Han', '⡽']
+ - ['han', '⠽']
+ - ['Efter', '⡵']
+ - ['efter', '⠵']
+ - ['Være', '⡜']
+ - ['være', '⠜']
+ - ['Før', '⡪']
+ - ['før', '⠪']
+ - ['Så', '⡡']
+ - ['så', '⠡']
+ - ['Den', '⡯']
+ - ['den', '⠯']
+ - ['Der', '⡾']
+ - ['der', '⠾']
+ - ['Det', '⡮']
+ - ['det', '⠮']
+ - ['De', '⡹']
+ - ['de', '⠹']
+ - ['En', '⡣']
+ - ['en', '⠣']
+ - ['Er', '⡱']
+ - ['er', '⠱']
+ - ['Et', '⡬']
+ - ['et', '⠬']
+ - ['Gennem', '⡻']
+ - ['gennem', '⠻']
+ - ['Hvor', '⡌']
+ - ['hvor', '⠌']
+ - ['Men', '⡩']
+ - ['men', '⠩']
+ - ['Ned', '⡫']
+ - ['ned', '⠫']
+ - ['Ret', '⡷']
+ - ['ret', '⠷']
+ - ['Skal', '⡿']
+ - ['skal', '⠿']
+ - ['Te', '⡳']
+ - ['te', '⠳']
+ - ['Var', '⡼']
+ - ['var', '⠼']
+
+# Partword/nocross
+
+ - ['Denne', '⡯⠫']
+ - ['denne', '⠯⠫']
+ - ['Mændene', '⡍⠜⠝⠹⠫']
+ - ['mændene', '⠍⠜⠝⠹⠫']
+ - ['Derhos', '⡾⠓⠕⠎']
+ - ['derhos', '⠾⠓⠕⠎']
+ - ['Hunderace', '⡓⠥⠝⠹⠗⠁⠉⠑']
+ - ['hunderace', '⠓⠥⠝⠹⠗⠁⠉⠑']
+ - ['Dette', '⡮⠳']
+ - ['dette', '⠮⠳']
+ - ['Detalje', '⡹⠞⠁⠇⠚⠑']
+ - ['detalje', '⠹⠞⠁⠇⠚⠑']
+
+# Nocross multiple cells
+
+ - ['Endda', '⡑⠟⠙⠁']
+ - ['endda', '⠑⠟⠙⠁']
+ - ['Morgendag', '⡍⠭⠛⠣⠙⠁⠛']
+ - ['morgendag', '⠍⠭⠛⠣⠙⠁⠛']
+ - ['Gendanne', '⡛⠣⠙⠁⠝⠫']
+ - ['gendanne', '⠛⠣⠙⠁⠝⠫']
+ - ['Generelt', '⡻⠫⠷⠇⠞']
+ - ['generelt', '⠻⠫⠷⠇⠞']
+
+ - ['Fra!', '⡋⠗⠁⠖']
+ - ['fra!', '⠋⠗⠁⠖']
+ - ['!Fra', '⠰⠖⡖']
+ - ['!fra', '⠖⠋⠗⠁']
+ - ['''Af', '⠈⡴']
+ - ['''af', '⠈⠁⠋']
+
+# Capsnocont
+
+ - ['UNDER et', '⡥⡝⡙⡑⡗ ⠬']
+
+# No single cell contractions before or after dashes
+
+ - ['at-bliver', '⠁⠞⠤⠃']
+ - ['d-d-du', '⠰⠙⠤⠰⠙⠤⠙⠥']
+
+# Combinations with slashes and other punctuation signs
+
+ - ["at!", "⠁⠖"]
+ - ["bliver!", "⠃⠖"]
+ - ["og!", "⠉⠖"]
+ - ['Han/hun', '⡽⢌⠥']
+ - ['han/hun', '⠽⢌⠥']
+ - ['Over/under', '⡭⢌⠟']
+ - ['over/under', '⠭⢌⠟']
+ - ['Til/fra', '⡞⢌⠖']
+ - ['til/fra', '⠞⢌⠖']
+
+# Combinations which require letsign
+
+ - ['1st', '⢁⠰⠎⠞']
+ - ['2nd', '⢃⠝⠙']
+ - ['1A', '⢁⠰⡁']
+ - ['1a', '⢁⠰⠁']
+ - ['2B', '⢃⠰⡃']
+ - ['2b', '⢃⠰⠃']
+
+# Emphasis (cannot be back-translated)
+# Single letter emphasis currently fails due to contraction across emphasis.
+
+flags: {testmode: forward}
+tests:
+
+ - # Bold line
+ - En linje med fed.
+ - ⠨⡣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⡬ ⠨⠭⠙⠨ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⡬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - {typeform: {bold: ' + '}, xfail: true}
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠨⡣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠨
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⡬ ⠨⠭⠙⠨ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⡬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - {typeform: {italic: ' + '}, xfail: true}
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠨⡣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄⠨
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⡬ ⠨⠭⠙⠨ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⡬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - {typeform: {underline: ' + '}, xfail: true}
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠨⡣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠨
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⡬ ⠨⠭⠙⠨ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⡬ ⠃⠕⠛⠨⠎⠨⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - {typeform: {bold: ' + ', italic: ' + ', underline: ' + '}, xfail: true}
diff --git a/LibLouis.NET.Test/braille-specs/da-dk_1993.yaml b/LibLouis.NET.Test/braille-specs/da-dk_1993.yaml
new file mode 100644
index 0000000..a1d0cbf
--- /dev/null
+++ b/LibLouis.NET.Test/braille-specs/da-dk_1993.yaml
@@ -0,0 +1,5970 @@
+# This file contains tests for the complete Danish 1993 braille
+# standard: all grades (0, 1, 1.5 and 2), 6-dot and 8-dot, regular and
+# "literary" (not back-translatable). Previously there was one test
+# file for each variant, but now that there is a new standard and the
+# table for the old standard are pretty much frozen, it has been
+# decided to collect the tests in a single file to keep things
+# tidy. The dictionary tests were left in separate files.
+
+# There is a lot of repetition in this file which could be eliminated
+# with some refactoring, but because we're dealing with a deprecated
+# standard we're not going to bother.
+
+display: unicode-without-blank.dis
+
+#############
+### 6-DOT ###
+#############
+
+# -----------------
+# Grade 1 (regular)
+# -----------------
+
+# Round trip tests
+# Commented tests currently fail backwards but should be fixed.
+
+table: {language: da, grade: 1, dots: 6, direction: both, version: 1993, __assert-match: da-dk-g16_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠲"]
+ - ["%", "⠚⠴"]
+ - ["&", "⠯"]
+ - ["'", "⠈"]
+ - ["(", "⠦"]
+ - [")", "⠴"]
+ - ["*", "⠔"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+ - ["/", "⠌"]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["A", "⠨⠁"]
+ - ["B", "⠨⠃"]
+ - ["C", "⠨⠉"]
+ - ["D", "⠨⠙"]
+ - ["E", "⠨⠑"]
+ - ["F", "⠨⠋"]
+ - ["G", "⠨⠛"]
+ - ["H", "⠨⠓"]
+ - ["I", "⠨⠊"]
+ - ["J", "⠨⠚"]
+ - ["K", "⠨⠅"]
+ - ["L", "⠨⠇"]
+ - ["M", "⠨⠍"]
+ - ["N", "⠨⠝"]
+ - ["O", "⠨⠕"]
+ - ["P", "⠨⠏"]
+ - ["Q", "⠨⠟"]
+ - ["R", "⠨⠗"]
+ - ["S", "⠨⠎"]
+ - ["T", "⠨⠞"]
+ - ["U", "⠨⠥"]
+ - ["V", "⠨⠧"]
+ - ["W", "⠨⠺"]
+ - ["X", "⠨⠭"]
+ - ["Y", "⠨⠽"]
+ - ["Z", "⠨⠵"]
+ - ["[", "⠐⠦"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["a", "⠁"]
+ - ["b", "⠃"]
+ - ["c", "⠉"]
+ - ["d", "⠙"]
+ - ["e", "⠑"]
+ - ["f", "⠋"]
+ - ["g", "⠛"]
+ - ["h", "⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠚"]
+ - ["k", "⠅"]
+ - ["l", "⠇"]
+ - ["m", "⠍"]
+ - ["n", "⠝"]
+ - ["o", "⠕"]
+ - ["p", "⠏"]
+ - ["q", "⠟"]
+ - ["r", "⠗"]
+ - ["s", "⠎"]
+ - ["t", "⠞"]
+ - ["u", "⠥"]
+ - ["v", "⠧"]
+ - ["w", "⠺"]
+ - ["x", "⠭"]
+ - ["y", "⠽"]
+ - ["z", "⠵"]
+ - ["{", "⠘⠪"]
+ - ["|", "⠘⠸"]
+ - ["}", "⠘⠕"]
+ - ["€", "⠘⠑"]
+ - ["ƒ", "⠘⠋"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠨⠐⠎"]
+ - ["Ž", "⠨⠐⠵"]
+ - ["•", "⠘⠄"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["ž", "⠐⠵"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["§", "⠬"]
+ - ["©", "⠘⠉"]
+ - ["®", "⠘⠗"]
+ - ["°", "⠈⠴"]
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["µ", "⠐⠍"]
+ - ["¹", "⠼⠬⠁"]
+ - ["À", "⠨⠐⠁"]
+ - ["Å", "⠨⠡"]
+ - ["Æ", "⠨⠜"]
+ - ["Ç", "⠨⠐⠉"]
+ - ["É", "⠨⠐⠑"]
+ - ["Î", "⠨⠐⠊"]
+ - ["Ð", "⠨⠐⠙"]
+ - ["Ñ", "⠨⠐⠝"]
+ - ["Ô", "⠨⠐⠕"]
+ - ["Ø", "⠨⠪"]
+ - ["Û", "⠨⠐⠥"]
+ - ["Ü", "⠨⠳"]
+ - ["Ý", "⠨⠐⠽"]
+ - ["Þ", "⠨⠐⠞"]
+ - ["à", "⠐⠁"]
+ - ["å", "⠡"]
+ - ["æ", "⠜"]
+ - ["ç", "⠐⠉"]
+ - ["é", "⠐⠑"]
+ - ["î", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ô", "⠐⠕"]
+ - ["÷", "⠲"]
+ - ["ø", "⠪"]
+ - ["û", "⠐⠥"]
+ - ["ü", "⠳"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+
+# Misc. Unicode (most cannot be back-translated)
+# Some tests may be repetitions of tests above, since
+# some characters can occur both inside and outside the 8 bit range.
+# for accented letters in the range u+0080 - u+00ff: the letters
+# that are thought to occur most frequently in Danish texts are used
+# for back-translation.
+# For all accented letters above u+00ff, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ["\u0134\u0135", "⠨⠐⠚⠐⠚"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0192", "⠘⠋"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0392\u03b2", "⠨⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠨⠐⠛⠐⠛"]
+ - ["\u0397\u03b7", "⠨⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠨⠐⠹⠐⠹"]
+ - ["\u039a\u03ba", "⠨⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠨⠐⠇⠐⠇"]
+ - ["\u039e\u03be", "⠨⠐⠭⠐⠭"]
+ - ["\u03a0\u03c0", "⠨⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠨⠐⠗⠐⠗"]
+ - ["\u03a6\u03c6", "⠨⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠨⠐⠓⠐⠓"]
+ - ["\u03a9\u03c9", "⠨⠐⠺⠐⠺"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2193", "⠘⠟"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠨⠟⠥⠊⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍⠑⠙ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⠨⠺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏⠡ ⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Caps and mixed case
+
+ - ["Foobar", "⠨⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠠⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠠⠃⠁⠗"]
+
+# Times vs. bullit
+
+ - ["\u2022Bullit", "⠘⠄⠨⠃⠥⠇⠇⠊⠞"]
+# - ["2 \u00d7 2 = 4", "⠼⠃ ⠘⠄ ⠼⠃ ⠘⠶ ⠼⠙"]
+
+# Section sign
+
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+
+# Numbers and punctuation
+
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+ - ["J) j) %) ') \u2030)", "⠨⠚⠠⠴ ⠚⠠⠴ ⠚⠴⠠⠴ ⠈⠠⠴ ⠚⠴⠴⠠⠴"]
+
+ - ["\"quotes\"", "⠶⠟⠥⠕⠞⠑⠎⠶"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - [":-) :-(", "⠒⠤⠴ ⠒⠤⠦"]
+ - [";-) ;-(", "⠆⠤⠴ ⠆⠤⠦"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+# - [" -", " ⠤⠤"]
+ - [" a-", " ⠁⠤"]
+ - [" - ", " ⠤⠤ "]
+ - [" -a-", " ⠤⠁⠤"]
+ - [" ", " "]
+ - ["(parentheses)", "⠦⠏⠁⠗⠑⠝⠞⠓⠑⠎⠑⠎⠴"]
+
+# Digits and letters
+
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠠⠁"]
+
+# Multi-pass tests
+
+# - ["~x |z", "⠘⠠⠭ ⠘⠸⠵"]
+# - ["~X |Z", "⠘⠠⠨⠭ ⠘⠸⠨⠵"]
+ - ["5É", "⠼⠑⠨⠐⠑"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+ - ["`", "⠈"]
+ - ["~", "⠘⠠"]
+ - ["‚", "⠈"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‹", "⠈"]
+ - ["Œ", "⠨⠕⠑"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠠"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["Ÿ", "⠨⠐⠽"]
+ - ["¡", "⠲"]
+ - ["«", "⠶"]
+ - ["", "⠤⠤"]
+ - ["¯", "⠢"]
+ - ["±", "⠘⠖⠤"]
+ - ["´", "⠈"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["Á", "⠨⠐⠁"]
+ - ["Â", "⠨⠐⠁"]
+ - ["Ã", "⠨⠐⠁"]
+ - ["Ä", "⠨⠜"]
+ - ["È", "⠨⠐⠑"]
+ - ["Ê", "⠨⠐⠑"]
+ - ["Ë", "⠨⠐⠑"]
+ - ["Ì", "⠨⠐⠊"]
+ - ["Í", "⠨⠐⠊"]
+ - ["Ï", "⠨⠐⠊"]
+ - ["Ò", "⠨⠐⠕"]
+ - ["Ó", "⠨⠐⠕"]
+ - ["Õ", "⠨⠐⠕"]
+ - ["Ö", "⠨⠪"]
+ - ["×", "⠘⠄"]
+ - ["Ù", "⠨⠐⠥"]
+ - ["Ú", "⠨⠐⠥"]
+ - ["ß", "⠎⠎"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ä", "⠜"]
+ - ["è", "⠐⠑"]
+ - ["ê", "⠐⠑"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ö", "⠪"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠨⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠨⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠨⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠨⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠨⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠨⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠨⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠨⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠨⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠨⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠨⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠨⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠨⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠨⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠨⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠨⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠨⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠨⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠨⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠨⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠨⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠨⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠨⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠨⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠨⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠨⠊⠚⠊⠚"]
+ - ["\u0136\u0137", "⠨⠐⠅⠐⠅"]
+ - ["\u0139\u013a", "⠨⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠨⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠨⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠨⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠨⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠨⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠨⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠨⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠨⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠨⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠨⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠨⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠨⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠨⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠨⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠨⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠨⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠨⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠨⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠨⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠨⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠨⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠨⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠨⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠨⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠨⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠨⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠨⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠨⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠨⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠨⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠨⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠨⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠨⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠨⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u02dc", "⠘⠠"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠨⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠨⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠨⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠨⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠨⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠨⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠨⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠨⠐⠁⠐⠁"]
+ - ["\u0394\u03b4", "⠨⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠨⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠨⠐⠵⠐⠵"]
+ - ["\u0399\u03b9", "⠨⠐⠊⠐⠊"]
+ - ["\u039c\u03bc", "⠨⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠨⠐⠝⠐⠝"]
+ - ["\u039f\u03bf", "⠨⠐⠕⠐⠕"]
+ - ["\u03a3\u03c3", "⠨⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠨⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠨⠐⠥⠐⠥"]
+ - ["\u03a8\u03c8", "⠨⠐⠽⠐⠽"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2192", "⠘⠗"] # back-translates as "registered".
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Extra digits
+
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+
+ - ["\u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠚⠴⠴⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴"]
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⠨⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠨⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠨⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⠨⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠨⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠨⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⠨⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠨⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠨⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⠨⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠨⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠨⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# Braille patterns which back-translate to something other than the original
+# Typically, single character representations back-translating to strings with more than one character
+
+flags: {testmode: backward}
+tests:
+
+# Characters
+
+ - ["⠄⠄⠄", "..."]
+ - ["⠨⠕⠑", "Oe"]
+ - ["⠕⠑", "oe"]
+ - ["⠘⠖⠤", "±", {xfail: true}]
+ - ["⠼⠁⠌⠙", "1/4"]
+ - ["⠼⠁⠌⠃", "1/2"]
+ - ["⠼⠉⠌⠙", "3/4"]
+
+# Extra digits
+
+ - ["⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙", "1\u00bc + 1\u00bd = 2\u00be", {xfail: true}]
+
+# -------------------------------
+# Grade 1 literary (forward only)
+# -------------------------------
+
+table: {language: da, grade: 1, dots: 6, direction: forward, version: 1993, __assert-match: da-dk-g16-lit_1993.ctb}
+flags: {testmode: forward}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠲"]
+ - ["%", "⠚⠴"]
+ - ["&", "⠯"]
+ - ["'", "⠈"]
+ - ["(", "⠦"]
+ - [")", "⠴"]
+ - ["*", "⠔"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+ - ["/", "⠌"]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["A", "⠨⠁"]
+ - ["B", "⠨⠃"]
+ - ["C", "⠨⠉"]
+ - ["D", "⠨⠙"]
+ - ["E", "⠨⠑"]
+ - ["F", "⠨⠋"]
+ - ["G", "⠨⠛"]
+ - ["H", "⠨⠓"]
+ - ["I", "⠨⠊"]
+ - ["J", "⠨⠚"]
+ - ["K", "⠨⠅"]
+ - ["L", "⠨⠇"]
+ - ["M", "⠨⠍"]
+ - ["N", "⠨⠝"]
+ - ["O", "⠨⠕"]
+ - ["P", "⠨⠏"]
+ - ["Q", "⠨⠟"]
+ - ["R", "⠨⠗"]
+ - ["S", "⠨⠎"]
+ - ["T", "⠨⠞"]
+ - ["U", "⠨⠥"]
+ - ["V", "⠨⠧"]
+ - ["W", "⠨⠺"]
+ - ["X", "⠨⠭"]
+ - ["Y", "⠨⠽"]
+ - ["Z", "⠨⠵"]
+ - ["[", "⠐⠦"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["`", "⠈"]
+ - ["a", "⠁"]
+ - ["b", "⠃"]
+ - ["c", "⠉"]
+ - ["d", "⠙"]
+ - ["e", "⠑"]
+ - ["f", "⠋"]
+ - ["g", "⠛"]
+ - ["h", "⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠚"]
+ - ["k", "⠅"]
+ - ["l", "⠇"]
+ - ["m", "⠍"]
+ - ["n", "⠝"]
+ - ["o", "⠕"]
+ - ["p", "⠏"]
+ - ["q", "⠟"]
+ - ["r", "⠗"]
+ - ["s", "⠎"]
+ - ["t", "⠞"]
+ - ["u", "⠥"]
+ - ["v", "⠧"]
+ - ["w", "⠺"]
+ - ["x", "⠭"]
+ - ["y", "⠽"]
+ - ["z", "⠵"]
+ - ["{", "⠘⠪"]
+ - ["|", "⠘⠸"]
+ - ["}", "⠘⠕"]
+ - ["~", "⠘⠠"]
+ - ["€", "⠘⠑"]
+ - ["‚", "⠈"]
+ - ["ƒ", "⠘⠋"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠨⠐⠎"]
+ - ["‹", "⠈"]
+ - ["Œ", "⠨⠕⠑"]
+ - ["Ž", "⠨⠐⠵"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["•", "⠘⠄"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠠"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["ž", "⠐⠵"]
+ - ["Ÿ", "⠨⠐⠽"]
+ - ["¡", "⠲"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["§", "⠬"]
+ - ["©", "⠘⠉"]
+ - ["«", "⠶"]
+ - ["", "⠤⠤"]
+ - ["®", "⠘⠗"]
+ - ["¯", "⠢"]
+ - ["°", "⠈⠴"]
+ - ["±", "⠘⠖⠤"]
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["´", "⠈"]
+ - ["µ", "⠐⠍"]
+ - ["¹", "⠼⠬⠁"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["À", "⠨⠐⠁"]
+ - ["Á", "⠨⠐⠁"]
+ - ["Â", "⠨⠐⠁"]
+ - ["Ã", "⠨⠐⠁"]
+ - ["Ä", "⠨⠜"]
+ - ["Å", "⠨⠡"]
+ - ["Æ", "⠨⠜"]
+ - ["Ç", "⠨⠐⠉"]
+ - ["È", "⠨⠐⠑"]
+ - ["É", "⠨⠐⠑"]
+ - ["Ê", "⠨⠐⠑"]
+ - ["Ë", "⠨⠐⠑"]
+ - ["Ì", "⠨⠐⠊"]
+ - ["Í", "⠨⠐⠊"]
+ - ["Î", "⠨⠐⠊"]
+ - ["Ï", "⠨⠐⠊"]
+ - ["Ð", "⠨⠐⠙"]
+ - ["Ñ", "⠨⠐⠝"]
+ - ["Ò", "⠨⠐⠕"]
+ - ["Ó", "⠨⠐⠕"]
+ - ["Ô", "⠨⠐⠕"]
+ - ["Õ", "⠨⠐⠕"]
+ - ["Ö", "⠨⠪"]
+ - ["×", "⠘⠄"]
+ - ["Ø", "⠨⠪"]
+ - ["Ù", "⠨⠐⠥"]
+ - ["Ú", "⠨⠐⠥"]
+ - ["Û", "⠨⠐⠥"]
+ - ["Ü", "⠨⠳"]
+ - ["Ý", "⠨⠐⠽"]
+ - ["Þ", "⠨⠐⠞"]
+ - ["ß", "⠎⠎"]
+ - ["à", "⠐⠁"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ä", "⠜"]
+ - ["å", "⠡"]
+ - ["æ", "⠜"]
+ - ["ç", "⠐⠉"]
+ - ["è", "⠐⠑"]
+ - ["é", "⠐⠑"]
+ - ["ê", "⠐⠑"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["î", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["ô", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ö", "⠪"]
+ - ["÷", "⠲"]
+ - ["ø", "⠪"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["û", "⠐⠥"]
+ - ["ü", "⠳"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠊⠚⠊⠚"]
+ - ["\u0134\u0135", "⠐⠚⠐⠚"]
+ - ["\u0136\u0137", "⠐⠅⠐⠅"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0139\u013a", "⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u0192", "⠘⠋"]
+ - ["\u02dc", "⠘⠠"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠐⠁⠐⠁"]
+ - ["\u0392\u03b2", "⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠐⠛⠐⠛"]
+ - ["\u0394\u03b4", "⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠐⠵⠐⠵"]
+ - ["\u0397\u03b7", "⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠐⠹⠐⠹"]
+ - ["\u0399\u03b9", "⠐⠊⠐⠊"]
+ - ["\u039a\u03ba", "⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠐⠇⠐⠇"]
+ - ["\u039c\u03bc", "⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠐⠝⠐⠝"]
+ - ["\u039e\u03be", "⠐⠭⠐⠭"]
+ - ["\u039f\u03bf", "⠐⠕⠐⠕"]
+ - ["\u03a0\u03c0", "⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠐⠗⠐⠗"]
+ - ["\u03a3\u03c3", "⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠐⠥⠐⠥"]
+ - ["\u03a6\u03c6", "⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠐⠓⠐⠓"]
+ - ["\u03a8\u03c8", "⠐⠽⠐⠽"]
+ - ["\u03a9\u03c9", "⠐⠺⠐⠺"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2192", "⠘⠗"]
+ - ["\u2193", "⠘⠟"]
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠟⠥⠊⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍⠑⠙ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⠺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏⠡ ⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⠑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# Caps and mixed case
+
+ - ["Foobar", "⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠠⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠠⠃⠁⠗"]
+
+# Extra digits
+
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠖⠼⠁⠼⠁⠌⠃ ⠶⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+
+# Times vs. bullit
+
+ - ["\u2022Bullit", "⠘⠄⠃⠥⠇⠇⠊⠞"]
+ - ["2 \u00d7 2 = 4", "⠼⠃ ⠄⠼⠃ ⠶⠼⠙"]
+
+# Section sign
+
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+
+# Numbers and punctuation
+
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+ - ["2\u00d72", "⠼⠃⠘⠄⠼⠃"]
+
+ - ["\"quotes\"", "⠶⠟⠥⠕⠞⠑⠎⠶"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - [":-) :-(", "⠒⠤⠴ ⠒⠤⠦"]
+ - [";-) ;-(", "⠆⠤⠴ ⠆⠤⠦"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+ - [" -", " ⠤⠤"]
+ - [" a-", " ⠁⠤"]
+ - [" - ", " ⠤⠤ "]
+ - [" -a-", " ⠤⠁⠤"]
+ - [" ", " "]
+ - ["(parentheses)", "⠦⠏⠁⠗⠑⠝⠞⠓⠑⠎⠑⠎⠴"]
+ - ["J) j) %) ') \u2030) \u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠨⠚⠠⠴ ⠚⠠⠴ ⠚⠴⠠⠴ ⠈⠠⠴ ⠚⠴⠴⠠⠴ ⠚⠴⠴⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴"]
+
+# Digits and letters
+
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠠⠁"]
+
+# Multi-pass tests
+
+ - ["~x |z", "⠘⠠⠭ ⠘⠸⠵"]
+ - ["~X |Z", "⠘⠠⠨⠭ ⠘⠸⠨⠵"]
+ - ["5É", "⠼⠑⠨⠐⠑"]
+
+# -------------------
+# Grade 1.5 (regular)
+# -------------------
+
+# Round trip tests
+# Commented tests currently fail backwards but should be fixed.
+
+table: {language: da, grade: 1.5, dots: 6, direction: both, version: 1993, __assert-match: da-dk-g26l_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠲"]
+ - ["%", "⠚⠴"]
+ - ["&", "⠠⠯"]
+ - ["'", "⠈"]
+ - ["(", "⠠⠦"]
+ - [")", "⠠⠴"]
+ - ["*", "⠠⠔"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+# - ["/", "⠌"]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["A", "⠨⠠⠁"]
+ - ["B", "⠨⠠⠃"]
+ - ["C", "⠨⠠⠉"]
+ - ["D", "⠨⠠⠙"]
+ - ["E", "⠨⠠⠑"]
+ - ["F", "⠨⠠⠋"]
+ - ["G", "⠨⠠⠛"]
+ - ["H", "⠨⠠⠓"]
+ - ["I", "⠨⠊"]
+ - ["J", "⠨⠠⠚"]
+ - ["K", "⠨⠠⠅"]
+ - ["L", "⠨⠠⠇"]
+ - ["M", "⠨⠠⠍"]
+ - ["N", "⠨⠠⠝"]
+ - ["O", "⠨⠠⠕"]
+ - ["P", "⠨⠠⠏"]
+ - ["Q", "⠨⠠⠟"]
+ - ["R", "⠨⠠⠗"]
+ - ["S", "⠨⠠⠎"]
+ - ["T", "⠨⠠⠞"]
+ - ["U", "⠨⠠⠥"]
+ - ["V", "⠨⠠⠧"]
+ - ["W", "⠨⠠⠺"]
+ - ["X", "⠨⠠⠭"]
+ - ["Y", "⠨⠠⠽"]
+ - ["Z", "⠨⠠⠵"]
+ - ["[", "⠐⠦"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["a", "⠠⠁"]
+ - ["b", "⠠⠃"]
+ - ["c", "⠠⠉"]
+ - ["d", "⠠⠙"]
+ - ["e", "⠠⠑"]
+ - ["f", "⠠⠋"]
+ - ["g", "⠠⠛"]
+ - ["h", "⠠⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠠⠚"]
+ - ["k", "⠠⠅"]
+ - ["l", "⠠⠇"]
+ - ["m", "⠠⠍"]
+ - ["n", "⠠⠝"]
+ - ["o", "⠠⠕"]
+ - ["p", "⠠⠏"]
+ - ["q", "⠠⠟"]
+ - ["r", "⠠⠗"]
+ - ["s", "⠠⠎"]
+ - ["t", "⠠⠞"]
+ - ["u", "⠠⠥"]
+ - ["v", "⠠⠧"]
+ - ["w", "⠠⠺"]
+ - ["x", "⠠⠭"]
+ - ["y", "⠠⠽"]
+ - ["z", "⠠⠵"]
+ - ["{", "⠘⠪"]
+ - ["|", "⠘⠸"]
+ - ["}", "⠘⠕"]
+ - ["~", "⠘⠠"]
+ - ["€", "⠘⠑"]
+ - ["ƒ", "⠘⠋"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠨⠐⠎"]
+ - ["Ž", "⠨⠐⠵"]
+ - ["•", "⠘⠄"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["ž", "⠐⠵"]
+ - ["¡", "⠠⠲"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["©", "⠘⠉"]
+ - ["®", "⠘⠗"]
+ - ["°", "⠈⠴"]
+ - ["µ", "⠐⠍"]
+ - ["À", "⠨⠐⠁"]
+ - ["Å", "⠨⠠⠡"]
+ - ["Æ", "⠨⠠⠜"]
+ - ["Ç", "⠨⠐⠉"]
+ - ["É", "⠨⠐⠑"]
+ - ["Î", "⠨⠐⠊"]
+ - ["Ð", "⠨⠐⠙"]
+ - ["Ñ", "⠨⠐⠝"]
+ - ["Ô", "⠨⠐⠕"]
+ - ["Ø", "⠨⠠⠪"]
+ - ["Û", "⠨⠐⠥"]
+ - ["Ü", "⠨⠠⠳"]
+ - ["Ý", "⠨⠐⠽"]
+ - ["Þ", "⠨⠐⠞"]
+ - ["à", "⠐⠁"]
+ - ["å", "⠠⠡"]
+ - ["æ", "⠠⠜"]
+ - ["ç", "⠐⠉"]
+ - ["é", "⠐⠑"]
+ - ["î", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ô", "⠐⠕"]
+ - ["ø", "⠠⠪"]
+ - ["û", "⠐⠥"]
+ - ["ü", "⠠⠳"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+
+# Misc. Unicode (most cannot be back-translated)
+# Some tests may be repetitions of tests above, since
+# some characters can occur both inside and outside the 8 bit range.
+# for accented letters in the range u+0080 - u+00ff: the letters
+# that are thought to occur most frequently in Danish texts are used
+# for back-translation.
+# For all accented letters above u+00ff, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ["\u0134\u0135", "⠨⠐⠚⠐⠚"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0192", "⠘⠋"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0392\u03b2", "⠨⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠨⠐⠛⠐⠛"]
+ - ["\u0397\u03b7", "⠨⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠨⠐⠹⠐⠹"]
+ - ["\u039a\u03ba", "⠨⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠨⠐⠇⠐⠇"]
+ - ["\u039e\u03be", "⠨⠐⠭⠐⠭"]
+ - ["\u03a0\u03c0", "⠨⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠨⠐⠗⠐⠗"]
+ - ["\u03a6\u03c6", "⠨⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠨⠐⠓⠐⠓"]
+ - ["\u03a9\u03c9", "⠨⠐⠺⠐⠺"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2193", "⠘⠟"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠨⠠⠟⠥⠊⠠⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⠨⠠⠺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏ ⠠⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Caps and mixed case
+
+ - ["Foobar", "⠨⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠠⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠠⠃⠁⠗"]
+
+# Percent and permille
+
+ - ["1%", "⠼⠁ ⠚⠴"]
+
+# Times vs. bullit
+
+ - ["\u2022Bullit", "⠘⠄⠨⠃⠥⠇⠇⠊⠞"]
+# - ["2 \u00d7 2 = 4", "⠼⠃ ⠘⠄ ⠼⠃ ⠘⠶ ⠼⠙"]
+
+# Section sign
+
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+
+# Numbers and punctuation
+
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+# - ["2\u00d72", "⠼⠃⠘⠄⠼⠃"]
+
+ - ["\"quotes\"", "⠶⠠⠟⠥⠕⠞⠑⠎⠶"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - [":-) :-(", "⠒⠤⠠⠴ ⠒⠤⠠⠦"]
+ - [";-) ;-(", "⠆⠤⠠⠴ ⠆⠤⠠⠦"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+# - [" -", " ⠤⠤"]
+ - [" a-", " ⠠⠁⠤"]
+ - [" - ", " ⠤⠤ "]
+ - [" -a-", " ⠤⠠⠁⠤"]
+ - [" ", " "]
+ - ["(parentheses)", "⠦⠏⠁⠗⠑⠝⠞⠓⠑⠎⠑⠎⠴"]
+
+# Exclamation
+
+ - ["\u00a1Que lastima!", "⠠⠲⠨⠠⠟⠥⠑ ⠇⠁⠎⠞⠊⠍⠁⠖"]
+
+# Digits and letters
+
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠠⠁"]
+
+# URLs emails and file names
+
+ - ["$at", "⠘⠲⠁⠞"]
+ - ["\u005c\u005cat\u005c\u005cbliver", "⠘⠡⠁⠞⠘⠡⠃⠇⠊⠧⠑⠗"]
+ - ["at@bliver.og", "⠁⠞⠘⠁⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["http://at.bliver.og", "⠓⠞⠞⠏⠒⠌⠌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.og", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.com", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍"]
+ - ["www.a.b.c", "⠠⠺⠠⠺⠠⠺⠄⠠⠁⠄⠠⠃⠄⠠⠉"]
+ - ["test.txt", "⠞⠑⠎⠞⠄⠞⠠⠭⠞"]
+
+# Word contractions
+
+ - ["At", "⠨⠁"]
+ - ["at", "⠁"]
+ - ["Bliver", "⠨⠃"]
+ - ["bliver", "⠃"]
+ - ["Og", "⠨⠉"]
+ - ["og", "⠉"]
+ - ["Du", "⠨⠙"]
+ - ["du", "⠙"]
+ - ["Eller", "⠨⠑"]
+ - ["eller", "⠑"]
+ - ["For", "⠨⠋"]
+ - ["for", "⠋"]
+ - ["Gør", "⠨⠛"]
+ - ["gør", "⠛"]
+ - ["Har", "⠨⠓"]
+ - ["har", "⠓"]
+ - ["Jeg", "⠨⠚"]
+ - ["jeg", "⠚"]
+ - ["Kan", "⠨⠅"]
+ - ["kan", "⠅"]
+ - ["Lige", "⠨⠇"]
+ - ["lige", "⠇"]
+ - ["Med", "⠨⠍"]
+ - ["med", "⠍"]
+ - ["Når", "⠨⠝"]
+ - ["når", "⠝"]
+ - ["Op", "⠨⠕"]
+ - ["op", "⠕"]
+ - ["På", "⠨⠏"]
+ - ["på", "⠏"]
+ - ["Under", "⠨⠟"]
+ - ["under", "⠟"]
+ - ["Rigtig", "⠨⠗"]
+ - ["rigtig", "⠗"]
+ - ["Som", "⠨⠎"]
+ - ["som", "⠎"]
+ - ["Til", "⠨⠞"]
+ - ["til", "⠞"]
+ - ["Hun", "⠨⠥"]
+ - ["hun", "⠥"]
+ - ["Ved", "⠨⠧"]
+ - ["ved", "⠧"]
+ - ["Hvad", "⠨⠺"]
+ - ["hvad", "⠺"]
+ - ["Over", "⠨⠭"]
+ - ["over", "⠭"]
+ - ["Han", "⠨⠽"]
+ - ["han", "⠽"]
+ - ["Efter", "⠨⠵"]
+ - ["efter", "⠵"]
+ - ["Være", "⠨⠜"]
+ - ["være", "⠜"]
+ - ["Før", "⠨⠪"]
+ - ["før", "⠪"]
+ - ["Så", "⠨⠡"]
+ - ["så", "⠡"]
+ - ["Den", "⠨⠯"]
+ - ["den", "⠯"]
+ - ["Der", "⠨⠾"]
+ - ["der", "⠾"]
+ - ["Det", "⠨⠮"]
+ - ["det", "⠮"]
+ - ["De", "⠨⠹"]
+ - ["de", "⠹"]
+ - ["En", "⠨⠣"]
+ - ["en", "⠣"]
+ - ["Er", "⠨⠱"]
+ - ["er", "⠱"]
+ - ["Et", "⠨⠬"]
+ - ["et", "⠬"]
+ - ["Gennem", "⠨⠻"]
+ - ["gennem", "⠻"]
+ - ["Hvor", "⠨⠌"]
+ - ["hvor", "⠌"]
+ - ["Men", "⠨⠩"]
+ - ["men", "⠩"]
+ - ["Ned", "⠨⠫"]
+ - ["ned", "⠫"]
+ - ["Ret", "⠨⠷"]
+ - ["ret", "⠷"]
+ - ["Skal", "⠨⠿"]
+ - ["skal", "⠿"]
+ - ["Te", "⠨⠳"]
+ - ["te", "⠳"]
+ - ["Ve", "⠨⠼"]
+ - ["ve", "⠼"]
+
+# No single cell contractions before or after dashes
+
+ - ["at-bliver", "⠁⠞⠤⠃"]
+ - ["d-d-du", "⠠⠙⠤⠠⠙⠤⠙⠥"]
+
+# Combinations with slashes and other punctuation signs
+
+ - ["Han/hun", "⠨⠽⠌⠥"]
+ - ["han/hun", "⠽⠌⠥"]
+ - ["Over/under", "⠨⠭⠌⠟"]
+ - ["over/under", "⠭⠌⠟"]
+ - ["Til/fra", "⠨⠞⠌⠖"]
+ - ["til/fra", "⠞⠌⠖"]
+
+# Combinations which require letsign
+
+ - ["1st", "⠼⠁⠠⠎⠞"]
+ - ["2nd", "⠼⠃⠠⠝⠙"]
+ - ["1A", "⠼⠁⠨⠠⠁", {xfail: {forward: true}}] # unclear spec
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["2B", "⠼⠃⠨⠠⠃", {xfail: {forward: true}}] # unclear spec
+ - ["2b", "⠼⠃⠠⠃"]
+
+# Multi-pass tests
+
+ - ["~x |z", "⠘⠠⠠⠭ ⠘⠸⠠⠵"]
+ - ["~X |Z", "⠘⠠⠨⠠⠭ ⠘⠸⠨⠠⠵"]
+ - ["5É", "⠼⠑⠨⠐⠑"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+ - ["`", "⠈"]
+ - ["‚", "⠈"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‹", "⠈"]
+ - ["Œ", "⠨⠕⠑"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠠"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["Ÿ", "⠨⠐⠽"]
+ - ["§", "⠬"]
+ - ["«", "⠶"]
+ - ["±", "⠘⠖⠤"]
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["´", "⠈"]
+ - ["¹", "⠼⠬⠁"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["Á", "⠨⠐⠁"]
+ - ["Â", "⠨⠐⠁"]
+ - ["Ã", "⠨⠐⠁"]
+ - ["Ä", "⠨⠠⠜"]
+ - ["È", "⠨⠐⠑"]
+ - ["Ê", "⠨⠐⠑"]
+ - ["Ë", "⠨⠐⠑"]
+ - ["Ì", "⠨⠐⠊"]
+ - ["Í", "⠨⠐⠊"]
+ - ["Ï", "⠨⠐⠊"]
+ - ["Ò", "⠨⠐⠕"]
+ - ["Ó", "⠨⠐⠕"]
+ - ["Õ", "⠨⠐⠕"]
+ - ["Ö", "⠨⠠⠪"]
+ - ["×", "⠘⠄"]
+ - ["Ù", "⠨⠐⠥"]
+ - ["Ú", "⠨⠐⠥"]
+ - ["ß", "⠎⠎"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ä", "⠠⠜"]
+ - ["è", "⠐⠑"]
+ - ["ê", "⠐⠑"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ö", "⠠⠪"]
+ - ["÷", "⠘⠲"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠨⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠨⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠨⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠨⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠨⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠨⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠨⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠨⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠨⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠨⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠨⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠨⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠨⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠨⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠨⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠨⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠨⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠨⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠨⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠨⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠨⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠨⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠨⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠨⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠨⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠨⠊⠚⠊⠚"]
+ - ["\u0136\u0137", "⠨⠐⠅⠐⠅"]
+ - ["\u0139\u013a", "⠨⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠨⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠨⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠨⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠨⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠨⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠨⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠨⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠨⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠨⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠨⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠨⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠨⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠨⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠨⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠨⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠨⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠨⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠨⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠨⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠨⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠨⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠨⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠨⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠨⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠨⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠨⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠨⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠨⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠨⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠨⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠨⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠨⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠨⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠨⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u02dc", "⠘⠠"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠨⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠨⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠨⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠨⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠨⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠨⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠨⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠨⠐⠁⠐⠁"]
+ - ["\u0394\u03b4", "⠨⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠨⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠨⠐⠵⠐⠵"]
+ - ["\u0399\u03b9", "⠨⠐⠊⠐⠊"]
+ - ["\u039c\u03bc", "⠨⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠨⠐⠝⠐⠝"]
+ - ["\u039f\u03bf", "⠨⠐⠕⠐⠕"]
+ - ["\u03a3\u03c3", "⠨⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠨⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠨⠐⠥⠐⠥"]
+ - ["\u03a8\u03c8", "⠨⠐⠽⠐⠽"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2192", "⠘⠗"] # back-translates as "registered".
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Extra digits
+
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+ - ["J) j) %) ') \u2030) \u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠨⠠⠚⠠⠴ ⠠⠚⠠⠴ ⠚⠴⠠⠴ ⠈⠠⠴ ⠚⠴⠴⠠⠴ ⠚⠴⠴⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴"]
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠨⠬ ⠰⠕⠗⠙⠰ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠨⠬ ⠰⠕⠗⠙⠰ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠨⠬ ⠰⠕⠗⠙⠰ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠨⠬ ⠰⠕⠗⠙⠰ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+flags: {testmode: backward}
+tests:
+
+# Characters
+
+ - ["⠄⠄⠄", "..."]
+ - ["⠨⠕⠑", "Oe"]
+ - ["⠕⠑", "oe"]
+ - ["⠘⠖⠤", "±", {xfail: true}]
+ - ["⠼⠁⠌⠙", "1/4"]
+ - ["⠼⠁⠌⠃", "1/2"]
+ - ["⠼⠉⠌⠙", "3/4"]
+
+# Extra digits
+
+ - ["⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙", "1\u00bc + 1\u00bd = 2\u00be", {xfail: true}]
+
+# ---------------------------------
+# Grade 1.5 literary (forward only)
+# ---------------------------------
+
+table: {language: da, grade: 1.5, dots: 6, direction: forward, version: 1993, __assert-match: da-dk-g26l-lit_1993.ctb}
+flags: {testmode: forward}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠲"]
+ - ["%", "⠚⠴"]
+ - ["&", "⠠⠯"]
+ - ["'", "⠈"]
+ - ["(", "⠠⠦"]
+ - [")", "⠠⠴"]
+ - ["*", "⠠⠔"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+ - ["/", "⠌"]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["A", "⠨⠠⠁"]
+ - ["B", "⠨⠠⠃"]
+ - ["C", "⠨⠠⠉"]
+ - ["D", "⠨⠠⠙"]
+ - ["E", "⠨⠠⠑"]
+ - ["F", "⠨⠠⠋"]
+ - ["G", "⠨⠠⠛"]
+ - ["H", "⠨⠠⠓"]
+ - ["I", "⠨⠊"]
+ - ["J", "⠨⠠⠚"]
+ - ["K", "⠨⠠⠅"]
+ - ["L", "⠨⠠⠇"]
+ - ["M", "⠨⠠⠍"]
+ - ["N", "⠨⠠⠝"]
+ - ["O", "⠨⠠⠕"]
+ - ["P", "⠨⠠⠏"]
+ - ["Q", "⠨⠠⠟"]
+ - ["R", "⠨⠠⠗"]
+ - ["S", "⠨⠠⠎"]
+ - ["T", "⠨⠠⠞"]
+ - ["U", "⠨⠠⠥"]
+ - ["V", "⠨⠠⠧"]
+ - ["W", "⠨⠠⠺"]
+ - ["X", "⠨⠠⠭"]
+ - ["Y", "⠨⠠⠽"]
+ - ["Z", "⠨⠠⠵"]
+ - ["[", "⠐⠦"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["`", "⠈"]
+ - ["a", "⠠⠁"]
+ - ["b", "⠠⠃"]
+ - ["c", "⠠⠉"]
+ - ["d", "⠠⠙"]
+ - ["e", "⠠⠑"]
+ - ["f", "⠠⠋"]
+ - ["g", "⠠⠛"]
+ - ["h", "⠠⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠠⠚"]
+ - ["k", "⠠⠅"]
+ - ["l", "⠠⠇"]
+ - ["m", "⠠⠍"]
+ - ["n", "⠠⠝"]
+ - ["o", "⠠⠕"]
+ - ["p", "⠠⠏"]
+ - ["q", "⠠⠟"]
+ - ["r", "⠠⠗"]
+ - ["s", "⠠⠎"]
+ - ["t", "⠠⠞"]
+ - ["u", "⠠⠥"]
+ - ["v", "⠠⠧"]
+ - ["w", "⠠⠺"]
+ - ["x", "⠠⠭"]
+ - ["y", "⠠⠽"]
+ - ["z", "⠠⠵"]
+ - ["{", "⠘⠪"]
+ - ["|", "⠘⠸"]
+ - ["}", "⠘⠕"]
+ - ["~", "⠘⠠"]
+ - ["€", "⠘⠑"]
+ - ["‚", "⠈"]
+ - ["ƒ", "⠘⠋"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠨⠐⠎"]
+ - ["‹", "⠈"]
+ - ["Œ", "⠨⠕⠑"]
+ - ["Ž", "⠨⠐⠵"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["•", "⠘⠄"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠠"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["ž", "⠐⠵"]
+ - ["Ÿ", "⠨⠐⠽"]
+ - ["¡", "⠠⠲"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["§", "⠬"]
+ - ["©", "⠘⠉"]
+ - ["«", "⠶"]
+ - ["®", "⠘⠗"]
+ - ["°", "⠈⠴"]
+ - ["±", "⠘⠖⠤"]
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["´", "⠈"]
+ - ["µ", "⠐⠍"]
+ - ["¹", "⠼⠬⠁"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["À", "⠨⠐⠁"]
+ - ["Á", "⠨⠐⠁"]
+ - ["Â", "⠨⠐⠁"]
+ - ["Ã", "⠨⠐⠁"]
+ - ["Ä", "⠨⠠⠜"]
+ - ["Å", "⠨⠠⠡"]
+ - ["Æ", "⠨⠠⠜"]
+ - ["Ç", "⠨⠐⠉"]
+ - ["È", "⠨⠐⠑"]
+ - ["É", "⠨⠐⠑"]
+ - ["Ê", "⠨⠐⠑"]
+ - ["Ë", "⠨⠐⠑"]
+ - ["Ì", "⠨⠐⠊"]
+ - ["Í", "⠨⠐⠊"]
+ - ["Î", "⠨⠐⠊"]
+ - ["Ï", "⠨⠐⠊"]
+ - ["Ð", "⠨⠐⠙"]
+ - ["Ñ", "⠨⠐⠝"]
+ - ["Ò", "⠨⠐⠕"]
+ - ["Ó", "⠨⠐⠕"]
+ - ["Ô", "⠨⠐⠕"]
+ - ["Õ", "⠨⠐⠕"]
+ - ["Ö", "⠨⠠⠪"]
+ - ["×", "⠘⠄"]
+ - ["Ø", "⠨⠠⠪"]
+ - ["Ù", "⠨⠐⠥"]
+ - ["Ú", "⠨⠐⠥"]
+ - ["Û", "⠨⠐⠥"]
+ - ["Ü", "⠨⠠⠳"]
+ - ["Ý", "⠨⠐⠽"]
+ - ["Þ", "⠨⠐⠞"]
+ - ["ß", "⠎⠎"]
+ - ["à", "⠐⠁"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ä", "⠠⠜"]
+ - ["å", "⠠⠡"]
+ - ["æ", "⠠⠜"]
+ - ["ç", "⠐⠉"]
+ - ["è", "⠐⠑"]
+ - ["é", "⠐⠑"]
+ - ["ê", "⠐⠑"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["î", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["ô", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ö", "⠠⠪"]
+ - ["÷", "⠘⠲"]
+ - ["ø", "⠠⠪"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["û", "⠐⠥"]
+ - ["ü", "⠠⠳"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠊⠚⠊⠚"]
+ - ["\u0134\u0135", "⠐⠚⠐⠚"]
+ - ["\u0136\u0137", "⠐⠅⠐⠅"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0139\u013a", "⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u0192", "⠘⠋"]
+ - ["\u02dc", "⠘⠠"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠐⠁⠐⠁"]
+ - ["\u0392\u03b2", "⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠐⠛⠐⠛"]
+ - ["\u0394\u03b4", "⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠐⠵⠐⠵"]
+ - ["\u0397\u03b7", "⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠐⠹⠐⠹"]
+ - ["\u0399\u03b9", "⠐⠊⠐⠊"]
+ - ["\u039a\u03ba", "⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠐⠇⠐⠇"]
+ - ["\u039c\u03bc", "⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠐⠝⠐⠝"]
+ - ["\u039e\u03be", "⠐⠭⠐⠭"]
+ - ["\u039f\u03bf", "⠐⠕⠐⠕"]
+ - ["\u03a0\u03c0", "⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠐⠗⠐⠗"]
+ - ["\u03a3\u03c3", "⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠐⠥⠐⠥"]
+ - ["\u03a6\u03c6", "⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠐⠓⠐⠓"]
+ - ["\u03a8\u03c8", "⠐⠽⠐⠽"]
+ - ["\u03a9\u03c9", "⠐⠺⠐⠺"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2192", "⠘⠗"]
+ - ["\u2193", "⠘⠟"]
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠠⠟⠥⠊⠠⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⠠⠺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏ ⠠⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠬ ⠰⠕⠗⠙⠰ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠬ ⠰⠕⠗⠙⠰ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠬ ⠰⠕⠗⠙⠰ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠬ ⠰⠕⠗⠙⠰ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+#caps and mixed case
+ - ["Foobar", "⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠠⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠠⠃⠁⠗"]
+
+# Percent and permille
+ - ["1%", "⠼⠁ ⠚⠴"]
+
+# Extra digits
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠖⠼⠁⠼⠁⠌⠃ ⠶⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+
+#Times vs. bullit
+ - ["\u2022Bullit", "⠘⠄⠃⠥⠇⠇⠊⠞"]
+ - ["2 \u00d7 2 = 4", "⠼⠃ ⠄⠼⠃ ⠶⠼⠙"]
+
+# Section sign
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+
+# numbers and punctuation
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+ - ["2\u00d72", "⠼⠃⠘⠄⠼⠃"]
+
+ - ["\"quotes\"", "⠶⠠⠟⠥⠕⠞⠑⠎⠶"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - [":-) :-(", "⠒⠤⠠⠴ ⠒⠤⠠⠦"]
+ - [";-) ;-(", "⠆⠤⠠⠴ ⠆⠤⠠⠦"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+ - [" -", " ⠤⠤"]
+ - [" a-", " ⠠⠁⠤"]
+ - [" - ", " ⠤⠤ "]
+ - [" -a-", " ⠤⠠⠁⠤"]
+ - [" ", " "]
+ - ["(parentheses)", "⠦⠏⠁⠗⠑⠝⠞⠓⠑⠎⠑⠎⠴"]
+ - ["J) j) %) ') \u2030) \u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠨⠠⠚⠠⠴ ⠠⠚⠠⠴ ⠚⠴⠠⠴ ⠈⠠⠴ ⠚⠴⠴⠠⠴ ⠚⠴⠴⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴"]
+
+ # Exclamation
+ - ["\u00a1Que lastima!", "⠠⠲⠠⠟⠥⠑ ⠇⠁⠎⠞⠊⠍⠁⠖"]
+
+# digits and letters
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠠⠁"]
+
+# URLs emails and file names
+
+ - ["$at", "⠘⠲⠁⠞"]
+ - ["\u005c\u005cat\u005c\u005cbliver", "⠘⠡⠁⠞⠘⠡⠃⠇⠊⠧⠑⠗"]
+ - ["at@bliver.og", "⠁⠞⠘⠁⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["http://at.bliver.og", "⠓⠞⠞⠏⠒⠌⠌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.og", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.com", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍"]
+ - ["www.a.b.c", "⠠⠺⠠⠺⠠⠺⠄⠠⠁⠄⠠⠃⠄⠠⠉"]
+ - ["test.txt", "⠞⠑⠎⠞⠄⠞⠠⠭⠞"]
+
+# word contractions
+
+ - ["At", "⠁"]
+ - ["at", "⠁"]
+ - ["Bliver", "⠃"]
+ - ["bliver", "⠃"]
+ - ["Og", "⠉"]
+ - ["og", "⠉"]
+ - ["Du", "⠙"]
+ - ["du", "⠙"]
+ - ["Eller", "⠑"]
+ - ["eller", "⠑"]
+ - ["For", "⠋"]
+ - ["for", "⠋"]
+ - ["Gør", "⠛"]
+ - ["gør", "⠛"]
+ - ["Har", "⠓"]
+ - ["har", "⠓"]
+ - ["Jeg", "⠚"]
+ - ["jeg", "⠚"]
+ - ["Kan", "⠅"]
+ - ["kan", "⠅"]
+ - ["Lige", "⠇"]
+ - ["lige", "⠇"]
+ - ["Med", "⠍"]
+ - ["med", "⠍"]
+ - ["Når", "⠝"]
+ - ["når", "⠝"]
+ - ["Op", "⠕"]
+ - ["op", "⠕"]
+ - ["På", "⠏"]
+ - ["på", "⠏"]
+ - ["Under", "⠟"]
+ - ["under", "⠟"]
+ - ["Rigtig", "⠗"]
+ - ["rigtig", "⠗"]
+ - ["Som", "⠎"]
+ - ["som", "⠎"]
+ - ["Til", "⠞"]
+ - ["til", "⠞"]
+ - ["Hun", "⠥"]
+ - ["hun", "⠥"]
+ - ["Ved", "⠧"]
+ - ["ved", "⠧"]
+ - ["Hvad", "⠺"]
+ - ["hvad", "⠺"]
+ - ["Over", "⠭"]
+ - ["over", "⠭"]
+ - ["Han", "⠽"]
+ - ["han", "⠽"]
+ - ["Efter", "⠵"]
+ - ["efter", "⠵"]
+ - ["Være", "⠜"]
+ - ["være", "⠜"]
+ - ["Før", "⠪"]
+ - ["før", "⠪"]
+ - ["Så", "⠡"]
+ - ["så", "⠡"]
+ - ["Den", "⠯"]
+ - ["den", "⠯"]
+ - ["Der", "⠾"]
+ - ["der", "⠾"]
+ - ["Det", "⠮"]
+ - ["det", "⠮"]
+ - ["De", "⠹"]
+ - ["de", "⠹"]
+ - ["En", "⠣"]
+ - ["en", "⠣"]
+ - ["Er", "⠱"]
+ - ["er", "⠱"]
+ - ["Et", "⠬"]
+ - ["et", "⠬"]
+ - ["Gennem", "⠻"]
+ - ["gennem", "⠻"]
+ - ["Hvor", "⠌"]
+ - ["hvor", "⠌"]
+ - ["Men", "⠩"]
+ - ["men", "⠩"]
+ - ["Ned", "⠫"]
+ - ["ned", "⠫"]
+ - ["Ret", "⠷"]
+ - ["ret", "⠷"]
+ - ["Skal", "⠿"]
+ - ["skal", "⠿"]
+ - ["Te", "⠳"]
+ - ["te", "⠳"]
+ - ["Ve", "⠼"]
+ - ["ve", "⠼"]
+
+# No single cell contractions before or after dashes
+
+ - ["at-bliver", "⠁⠞⠤⠃"]
+ - ["d-d-du", "⠠⠙⠤⠠⠙⠤⠙⠥"]
+
+# combinations with slashes and other punctuation signs
+
+ - ["at!", "⠁⠖"]
+ - ["bliver!", "⠃⠖"]
+ - ["og!", "⠉⠖"]
+ - ["Han/hun", "⠽⠌⠥"]
+ - ["han/hun", "⠽⠌⠥"]
+ - ["Over/under", "⠭⠌⠟"]
+ - ["over/under", "⠭⠌⠟"]
+ - ["Til/fra", "⠞⠌⠖"]
+ - ["til/fra", "⠞⠌⠖"]
+
+# Combinations which require letsign
+
+ - ["1st", "⠼⠁⠠⠎⠞"]
+ - ["2nd", "⠼⠃⠠⠝⠙"]
+ - ["1A", "⠼⠁⠨⠠⠁", {xfail: true}] # unclear spec
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["2B", "⠼⠃⠨⠠⠃", {xfail: true}] # unclear spec
+ - ["2b", "⠼⠃⠠⠃"]
+
+# Multi-pass tests
+ - ["~x |z", "⠘⠠⠠⠭ ⠘⠸⠠⠵"]
+ - ["~X |Z", "⠘⠠⠨⠠⠭ ⠘⠸⠨⠠⠵"]
+ - ["5É", "⠼⠑⠨⠐⠑"]
+
+# -----------------
+# Grade 2 (regular)
+# -----------------
+
+# Round trip tests
+# Commented tests currently fail backwards but should be fixed.
+
+table: {language: da, grade: 2, dots: 6, direction: both, version: 1993, __assert-match: da-dk-g26_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠲"]
+ - ["%", "⠚⠴"]
+ - ["&", "⠠⠯"]
+ - ["'", "⠈"]
+ - ["(", "⠠⠦"]
+ - [")", "⠠⠴"]
+ - ["*", "⠠⠔"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+ - ["/", "⠠⠌", {xfail: {forward: true}}]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["A", "⠨⠠⠁"]
+ - ["B", "⠨⠠⠃"]
+ - ["C", "⠨⠠⠉"]
+ - ["D", "⠨⠠⠙"]
+ - ["E", "⠨⠠⠑"]
+ - ["F", "⠨⠠⠋"]
+ - ["G", "⠨⠠⠛"]
+ - ["H", "⠨⠠⠓"]
+ - ["I", "⠨⠊"]
+ - ["J", "⠨⠠⠚"]
+ - ["K", "⠨⠠⠅"]
+ - ["L", "⠨⠠⠇"]
+ - ["M", "⠨⠠⠍"]
+ - ["N", "⠨⠠⠝"]
+ - ["O", "⠨⠠⠕"]
+ - ["P", "⠨⠠⠏"]
+ - ["Q", "⠨⠠⠟"]
+ - ["R", "⠨⠠⠗"]
+ - ["S", "⠨⠠⠎"]
+ - ["T", "⠨⠠⠞"]
+ - ["U", "⠨⠠⠥"]
+ - ["V", "⠨⠠⠧"]
+ - ["W", "⠨⠠⠺"]
+ - ["X", "⠨⠠⠭"]
+ - ["Y", "⠨⠠⠽"]
+ - ["Z", "⠨⠠⠵"]
+ - ["[", "⠐⠦"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["a", "⠠⠁"]
+ - ["b", "⠠⠃"]
+ - ["c", "⠠⠉"]
+ - ["d", "⠠⠙"]
+ - ["e", "⠠⠑"]
+ - ["f", "⠠⠋"]
+ - ["g", "⠠⠛"]
+ - ["h", "⠠⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠠⠚"]
+ - ["k", "⠠⠅"]
+ - ["l", "⠠⠇"]
+ - ["m", "⠠⠍"]
+ - ["n", "⠠⠝"]
+ - ["o", "⠠⠕"]
+ - ["p", "⠠⠏"]
+ - ["q", "⠠⠟"]
+ - ["r", "⠠⠗"]
+ - ["s", "⠠⠎"]
+ - ["t", "⠠⠞"]
+ - ["u", "⠠⠥"]
+ - ["v", "⠠⠧"]
+ - ["w", "⠠⠺"]
+ - ["x", "⠠⠭"]
+ - ["y", "⠠⠽"]
+ - ["z", "⠠⠵"]
+ - ["{", "⠘⠪"]
+ - ["|", "⠘⠸"]
+ - ["}", "⠘⠕"]
+ - ["~", "⠘⠠"]
+ - ["€", "⠘⠑"]
+ - ["ƒ", "⠘⠋"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠨⠐⠎"]
+ - ["Ž", "⠨⠐⠵"]
+ - ["•", "⠘⠄"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["ž", "⠐⠵"]
+ - ["¡", "⠠⠲"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["©", "⠘⠉"]
+ - ["®", "⠘⠗"]
+ - ["°", "⠈⠴"]
+ - ["µ", "⠐⠍"]
+ - ["À", "⠨⠐⠁"]
+ - ["Å", "⠨⠠⠡"]
+ - ["Æ", "⠨⠠⠜"]
+ - ["Ç", "⠨⠐⠉"]
+ - ["É", "⠨⠐⠑"]
+ - ["Î", "⠨⠐⠊"]
+ - ["Ð", "⠨⠐⠙"]
+ - ["Ñ", "⠨⠐⠝"]
+ - ["Ô", "⠨⠐⠕"]
+ - ["Ø", "⠨⠠⠪"]
+ - ["Û", "⠨⠐⠥"]
+ - ["Ü", "⠨⠠⠳"]
+ - ["Ý", "⠨⠐⠽"]
+ - ["Þ", "⠨⠐⠞"]
+ - ["à", "⠐⠁"]
+ - ["å", "⠠⠡"]
+ - ["æ", "⠠⠜"]
+ - ["ç", "⠐⠉"]
+ - ["é", "⠐⠑"]
+ - ["î", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ô", "⠐⠕"]
+ - ["ø", "⠠⠪"]
+ - ["û", "⠐⠥"]
+ - ["ü", "⠠⠳"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+
+# Misc. Unicode (most cannot be back-translated)
+# Some tests may be repetitions of tests above, since
+# some characters can occur both inside and outside the 8 bit range.
+# for accented letters in the range u+0080 - u+00ff: the letters
+# that are thought to occur most frequently in Danish texts are used
+# for back-translation.
+# For all accented letters above u+00ff, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ["\u0134\u0135", "⠨⠐⠚⠐⠚"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0192", "⠘⠋"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0392\u03b2", "⠨⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠨⠐⠛⠐⠛"]
+ - ["\u0397\u03b7", "⠨⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠨⠐⠹⠐⠹"]
+ - ["\u039a\u03ba", "⠨⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠨⠐⠇⠐⠇"]
+ - ["\u039e\u03be", "⠨⠐⠭⠐⠭"]
+ - ["\u03a0\u03c0", "⠨⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠨⠐⠗⠐⠗"]
+ - ["\u03a6\u03c6", "⠨⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠨⠐⠓⠐⠓"]
+ - ["\u03a9\u03c9", "⠨⠐⠺⠐⠺"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2193", "⠘⠟"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠨⠠⠟⠥⠊⠠⠵⠹⠇⠞⠁⠛⠱⠫ ⠎⠏⠊⠵⠑ ⠚⠭⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠹⠂ ⠍⠣⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠣ ⠨⠠⠺⠁⠇⠞⠓⠱ ⠎⠏⠊⠇⠇⠑⠹ ⠏ ⠠⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Caps and mixed case
+
+ - ["Foobar", "⠨⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠠⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠠⠃⠁⠗"]
+
+# Percent and permille
+
+ - ["1%", "⠼⠁ ⠚⠴"]
+
+# Times vs. bullit
+
+ - ["\u2022Bullit", "⠘⠄⠨⠃⠥⠇⠇⠊⠞"]
+# - ["2 \u00d7 2 = 4", "⠼⠃ ⠘⠄ ⠼⠃ ⠘⠶ ⠼⠙"]
+
+# Section sign
+
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+
+# Numbers and punctuation
+
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+# - ["2\u00d72", "⠼⠃⠘⠄⠼⠃"]
+
+ - ["\"quotes\"", "⠶⠠⠟⠥⠕⠳⠎⠶"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - [":-) :-(", "⠒⠤⠠⠴ ⠒⠤⠠⠦"]
+ - [";-) ;-(", "⠆⠤⠠⠴ ⠆⠤⠠⠦"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+# - [" -", " ⠤⠤"]
+ - [" a-", " ⠠⠁⠤"]
+ - [" - ", " ⠤⠤ "]
+ - [" -a-", " ⠤⠠⠁⠤"]
+ - [" ", " "]
+ - ["(parentheses)", "⠦⠏⠁⠗⠣⠞⠓⠑⠎⠑⠎⠴"]
+
+# Exclamation
+
+ - ["\u00a1Que lastima!", "⠠⠲⠨⠠⠟⠥⠑ ⠇⠁⠵⠊⠍⠁⠖"]
+
+# Digits and letters
+
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠠⠁"]
+
+# URLs emails and file names
+
+ - ["$at", "⠘⠲⠁⠞"]
+ - ["\u005c\u005cat\u005c\u005cbliver", "⠘⠡⠁⠞⠘⠡⠃⠇⠊⠧⠑⠗"]
+ - ["at@bliver.og", "⠁⠞⠘⠁⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["http://at.bliver.og", "⠓⠞⠞⠏⠒⠌⠌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.og", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.com", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍"]
+ - ["www.a.b.c", "⠠⠺⠠⠺⠠⠺⠄⠠⠁⠄⠠⠃⠄⠠⠉"]
+ - ["test.txt", "⠳⠵⠄⠞⠠⠭⠞"]
+
+# Word contractions
+
+ - ["At", "⠨⠁"]
+ - ["at", "⠁"]
+ - ["Aldrig", "⠨⠁⠔"]
+ - ["aldrig", "⠁⠔"]
+ - ["aig", "⠁⠊⠛"]
+ - ["Alle", "⠨⠁⠑"]
+ - ["alle", "⠁⠑"]
+ - ["ae", "⠠⠁⠑"]
+ - ["Allerede", "⠨⠁⠇⠗"]
+ - ["allerede", "⠁⠇⠗"]
+ - ["alr", "⠠⠁⠇⠗"]
+ - ["Alligevel", "⠨⠁⠇⠧"]
+ - ["alligevel", "⠁⠇⠧"]
+ - ["alv", "⠠⠁⠇⠧"]
+ - ["Altid", "⠨⠁⠞⠙"]
+ - ["altid", "⠁⠞⠙"]
+ - ["atd", "⠠⠁⠞⠙"]
+ - ["Altså", "⠨⠁⠡"]
+ - ["altså", "⠁⠡"]
+ - ["aå", "⠠⠁⠡"]
+ - ["Bliver", "⠨⠃"]
+ - ["bliver", "⠃"]
+ - ["Og", "⠨⠉"]
+ - ["og", "⠉"]
+ - ["Deres", "⠨⠲"]
+ - ["deres", "⠲"]
+ - ["Du", "⠨⠙"]
+ - ["du", "⠙"]
+ - ["Eller", "⠨⠑"]
+ - ["eller", "⠑"]
+ - ["For", "⠨⠋"]
+ - ["for", "⠋"]
+ - ["Gør", "⠨⠛"]
+ - ["gør", "⠛"]
+ - ["Har", "⠨⠓"]
+ - ["har", "⠓"]
+ - ["Jeg", "⠨⠚"]
+ - ["jeg", "⠚"]
+ - ["Kan", "⠨⠅"]
+ - ["kan", "⠅"]
+ - ["Lige", "⠨⠇"]
+ - ["lige", "⠇"]
+ - ["Med", "⠨⠍"]
+ - ["med", "⠍"]
+ - ["Når", "⠨⠝"]
+ - ["når", "⠝"]
+ - ["Op", "⠨⠕"]
+ - ["op", "⠕"]
+ - ["På", "⠨⠏"]
+ - ["på", "⠏"]
+ - ["Under", "⠨⠟"]
+ - ["under", "⠟"]
+ - ["Rigtig", "⠨⠗"]
+ - ["rigtig", "⠗"]
+ - ["Som", "⠨⠎"]
+ - ["som", "⠎"]
+ - ["Til", "⠨⠞"]
+ - ["til", "⠞"]
+ - ["Hun", "⠨⠥"]
+ - ["hun", "⠥"]
+ - ["Ved", "⠨⠧"]
+ - ["ved", "⠧"]
+ - ["Hvad", "⠨⠺"]
+ - ["hvad", "⠺"]
+ - ["Over", "⠨⠭"]
+ - ["over", "⠭"]
+ - ["Han", "⠨⠽"]
+ - ["han", "⠽"]
+ - ["Efter", "⠨⠵"]
+ - ["efter", "⠵"]
+ - ["Være", "⠨⠜"]
+ - ["være", "⠜"]
+ - ["Før", "⠨⠪"]
+ - ["før", "⠪"]
+ - ["Så", "⠨⠡"]
+ - ["så", "⠡"]
+ - ["Den", "⠨⠯"]
+ - ["den", "⠯"]
+ - ["Der", "⠨⠾"]
+ - ["der", "⠾"]
+ - ["Det", "⠨⠮"]
+ - ["det", "⠮"]
+ - ["De", "⠨⠹"]
+ - ["de", "⠹"]
+ - ["En", "⠨⠣"]
+ - ["en", "⠣"]
+ - ["Er", "⠨⠱"]
+ - ["er", "⠱"]
+ - ["Et", "⠨⠬"]
+ - ["et", "⠬"]
+ - ["Gennem", "⠨⠻"]
+ - ["gennem", "⠻"]
+ - ["Hvor", "⠨⠌"]
+ - ["hvor", "⠌"]
+ - ["Men", "⠨⠩"]
+ - ["men", "⠩"]
+ - ["Ned", "⠨⠫"]
+ - ["ned", "⠫"]
+ - ["Ret", "⠨⠷"]
+ - ["ret", "⠷"]
+ - ["Skal", "⠨⠿"]
+ - ["skal", "⠿"]
+ - ["Te", "⠨⠳"]
+ - ["te", "⠳"]
+ - ["Ve", "⠨⠼"]
+ - ["ve", "⠼"]
+
+# Partword/nocross
+
+ - ["Denne", "⠨⠯⠫"]
+ - ["denne", "⠯⠫"]
+ - ["Mændene", "⠨⠍⠜⠝⠹⠫"]
+ - ["mændene", "⠍⠜⠝⠹⠫"]
+ - ["Derhos", "⠨⠾⠓⠕⠎"]
+ - ["derhos", "⠾⠓⠕⠎"]
+ - ["Hunderace", "⠨⠓⠥⠝⠹⠗⠁⠉⠑"]
+ - ["hunderace", "⠓⠥⠝⠹⠗⠁⠉⠑"]
+ - ["Dette", "⠨⠮⠳"]
+ - ["dette", "⠮⠳"]
+ - ["Detalje", "⠨⠹⠞⠁⠇⠚⠑"]
+ - ["detalje", "⠹⠞⠁⠇⠚⠑"]
+
+# Nocross multiple cells
+
+ - ["Endda", "⠨⠑⠟⠙⠁"]
+ - ["endda", "⠑⠟⠙⠁"]
+ - ["Morgendag", "⠨⠍⠭⠛⠣⠙⠁⠛"]
+ - ["morgendag", "⠍⠭⠛⠣⠙⠁⠛"]
+ - ["Gendanne", "⠨⠛⠣⠙⠁⠝⠫"]
+ - ["gendanne", "⠛⠣⠙⠁⠝⠫"]
+ - ["Generelt", "⠨⠻⠫⠷⠇⠞"]
+ - ["generelt", "⠻⠫⠷⠇⠞"]
+
+ - ["Fra!", "⠨⠋⠗⠁⠖"]
+ - ["fra!", "⠋⠗⠁⠖"]
+ - ["!Fra", "⠠⠖⠨⠖"]
+ - ["!fra", "⠖⠋⠗⠁"]
+ - ["'Af", "⠈⠨⠴"]
+ - ["'af", "⠈⠁⠋"]
+
+# No single cell contractions before or after dashes
+
+ - ["at-bliver", "⠁⠞⠤⠃"]
+ - ["d-d-du", "⠠⠙⠤⠠⠙⠤⠙⠥"]
+
+# Combinations with slashes and other punctuation signs
+
+ - ["at!", "⠁⠖"]
+ - ["bliver!", "⠃⠖"]
+ - ["og!", "⠉⠖"]
+ - ["Han/hun", "⠨⠽⠌⠥"]
+ - ["han/hun", "⠽⠌⠥"]
+ - ["Over/under", "⠨⠭⠌⠟"]
+ - ["over/under", "⠭⠌⠟"]
+ - ["Til/fra", "⠨⠞⠌⠖"]
+ - ["til/fra", "⠞⠌⠖"]
+
+# Combinations which require letsign
+
+ - ["1st", "⠼⠁⠠⠎⠞"]
+ - ["2nd", "⠼⠃⠠⠝⠙"]
+ - ["1A", "⠼⠁⠨⠠⠁", {xfail: {forward: true}}] # unclear spec
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["2B", "⠼⠃⠨⠠⠃", {xfail: {forward: true}}] # unclear spec
+ - ["2b", "⠼⠃⠠⠃"]
+
+# Multi-pass tests
+
+ - ["~x |z", "⠘⠠⠠⠭ ⠘⠸⠠⠵"]
+ - ["~X |Z", "⠘⠠⠨⠠⠭ ⠘⠸⠨⠠⠵"]
+ - ["5É", "⠼⠑⠨⠐⠑"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+ - ["`", "⠈"]
+ - ["‚", "⠈"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‹", "⠈"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["Œ", "⠨⠕⠑"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠠"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["Ÿ", "⠨⠐⠽"]
+ - ["§", "⠬"]
+ - ["«", "⠶"]
+ - ["±", "⠘⠖⠤"]
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["´", "⠈"]
+ - ["¹", "⠼⠬⠁"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["Á", "⠨⠐⠁"]
+ - ["Â", "⠨⠐⠁"]
+ - ["Ã", "⠨⠐⠁"]
+ - ["Ä", "⠨⠠⠜"]
+ - ["È", "⠨⠐⠑"]
+ - ["Ê", "⠨⠐⠑"]
+ - ["Ë", "⠨⠐⠑"]
+ - ["Ì", "⠨⠐⠊"]
+ - ["Í", "⠨⠐⠊"]
+ - ["Ï", "⠨⠐⠊"]
+ - ["Ò", "⠨⠐⠕"]
+ - ["Ó", "⠨⠐⠕"]
+ - ["Õ", "⠨⠐⠕"]
+ - ["Ö", "⠨⠠⠪"]
+ - ["×", "⠘⠄"]
+ - ["Ù", "⠨⠐⠥"]
+ - ["Ú", "⠨⠐⠥"]
+ - ["ß", "⠎⠎"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ä", "⠠⠜"]
+ - ["è", "⠐⠑"]
+ - ["ê", "⠐⠑"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ö", "⠠⠪"]
+ - ["÷", "⠘⠲"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠨⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠨⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠨⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠨⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠨⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠨⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠨⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠨⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠨⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠨⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠨⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠨⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠨⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠨⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠨⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠨⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠨⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠨⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠨⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠨⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠨⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠨⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠨⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠨⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠨⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠨⠊⠚⠊⠚"]
+ - ["\u0136\u0137", "⠨⠐⠅⠐⠅"]
+ - ["\u0139\u013a", "⠨⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠨⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠨⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠨⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠨⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠨⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠨⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠨⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠨⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠨⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠨⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠨⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠨⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠨⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠨⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠨⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠨⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠨⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠨⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠨⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠨⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠨⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠨⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠨⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠨⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠨⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠨⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠨⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠨⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠨⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠨⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠨⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠨⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠨⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠨⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u02dc", "⠘⠠"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠨⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠨⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠨⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠨⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠨⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠨⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠨⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠨⠐⠁⠐⠁"]
+ - ["\u0394\u03b4", "⠨⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠨⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠨⠐⠵⠐⠵"]
+ - ["\u0399\u03b9", "⠨⠐⠊⠐⠊"]
+ - ["\u039c\u03bc", "⠨⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠨⠐⠝⠐⠝"]
+ - ["\u039f\u03bf", "⠨⠐⠕⠐⠕"]
+ - ["\u03a3\u03c3", "⠨⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠨⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠨⠐⠥⠐⠥"]
+ - ["\u03a8\u03c8", "⠨⠐⠽⠐⠽"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2192", "⠘⠗"] # back-translates as "registered".
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Extra digits
+
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+
+# Parentheses and misc
+
+ - ["J) j) %) ') \u2030) \u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠨⠠⠚⠠⠴ ⠠⠚⠠⠴ ⠚⠴⠠⠴ ⠈⠠⠴ ⠚⠴⠴⠠⠴ ⠚⠴⠴⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴"]
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠨⠬ ⠰⠭⠙⠰ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠨⠬ ⠰⠭⠙⠰ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠨⠬ ⠰⠭⠙⠰ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⠨⠣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠨⠬ ⠰⠭⠙⠰ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠨⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# Braille patterns which back-translate to something other than the original
+
+flags: {testmode: backward}
+tests:
+
+# Characters
+
+ - ["⠄⠄⠄", "..."]
+ - ["⠨⠕⠑", "Oe"]
+ - ["⠕⠑", "oe"]
+ - ["⠘⠖⠤", "±", {xfail: true}]
+ - ["⠼⠁⠌⠙", "1/4"]
+ - ["⠼⠁⠌⠃", "1/2"]
+ - ["⠼⠉⠌⠙", "3/4"]
+
+# Extra digits
+
+ - ["⠼⠁⠼⠁⠌⠙ ⠘⠖ ⠼⠁⠼⠁⠌⠃ ⠘⠶ ⠼⠃⠼⠉⠌⠙", "1\u00bc + 1\u00bd = 2\u00be", {xfail: true}]
+
+# -------------------------------
+# Grade 2 literary (forward only)
+# -------------------------------
+
+table: {language: da, grade: 2, dots: 6, direction: forward, version: 1993, __assert-match: da-dk-g26-lit_1993.ctb}
+flags: {testmode: forward}
+tests:
+
+# Characters
+
+ - [" ", " "]
+ - ["\"", "⠶"]
+ - ["\u0023", "⠘⠼"]
+ - ["$", "⠘⠲"]
+ - ["%", "⠚⠴"]
+ - ["&", "⠠⠯"]
+ - ["'", "⠈"]
+ - ["(", "⠠⠦"]
+ - [")", "⠠⠴"]
+ - ["*", "⠠⠔"]
+ - ["+", "⠘⠖"]
+ - [",", "⠂"]
+ - ["-", "⠤⠤"]
+ - [".", "⠄"]
+ - ["/", "⠠⠌", {xfail: true}]
+ - ["0", "⠼⠚"]
+ - ["1", "⠼⠁"]
+ - ["2", "⠼⠃"]
+ - ["3", "⠼⠉"]
+ - ["4", "⠼⠙"]
+ - ["5", "⠼⠑"]
+ - ["6", "⠼⠋"]
+ - ["7", "⠼⠛"]
+ - ["8", "⠼⠓"]
+ - ["9", "⠼⠊"]
+ - [":", "⠒"]
+ - [";", "⠆"]
+ - ["<", "⠘⠍"]
+ - ["=", "⠘⠶"]
+ - [">", "⠘⠎"]
+ - ["?", "⠢"]
+ - ["@", "⠘⠁"]
+ - ["A", "⠨⠠⠁"]
+ - ["B", "⠨⠠⠃"]
+ - ["C", "⠨⠠⠉"]
+ - ["D", "⠨⠠⠙"]
+ - ["E", "⠨⠠⠑"]
+ - ["F", "⠨⠠⠋"]
+ - ["G", "⠨⠠⠛"]
+ - ["H", "⠨⠠⠓"]
+ - ["I", "⠨⠊"]
+ - ["J", "⠨⠠⠚"]
+ - ["K", "⠨⠠⠅"]
+ - ["L", "⠨⠠⠇"]
+ - ["M", "⠨⠠⠍"]
+ - ["N", "⠨⠠⠝"]
+ - ["O", "⠨⠠⠕"]
+ - ["P", "⠨⠠⠏"]
+ - ["Q", "⠨⠠⠟"]
+ - ["R", "⠨⠠⠗"]
+ - ["S", "⠨⠠⠎"]
+ - ["T", "⠨⠠⠞"]
+ - ["U", "⠨⠠⠥"]
+ - ["V", "⠨⠠⠧"]
+ - ["W", "⠨⠠⠺"]
+ - ["X", "⠨⠠⠭"]
+ - ["Y", "⠨⠠⠽"]
+ - ["Z", "⠨⠠⠵"]
+ - ["[", "⠐⠦"]
+ - ["\u005c\u005c", "⠘⠡"]
+ - ["]", "⠐⠴"]
+ - ["^", "⠘⠬"]
+ - ["_", "⠘⠤"]
+ - ["`", "⠈"]
+ - ["a", "⠠⠁"]
+ - ["b", "⠠⠃"]
+ - ["c", "⠠⠉"]
+ - ["d", "⠠⠙"]
+ - ["e", "⠠⠑"]
+ - ["f", "⠠⠋"]
+ - ["g", "⠠⠛"]
+ - ["h", "⠠⠓"]
+ - ["i", "⠊"]
+ - ["j", "⠠⠚"]
+ - ["k", "⠠⠅"]
+ - ["l", "⠠⠇"]
+ - ["m", "⠠⠍"]
+ - ["n", "⠠⠝"]
+ - ["o", "⠠⠕"]
+ - ["p", "⠠⠏"]
+ - ["q", "⠠⠟"]
+ - ["r", "⠠⠗"]
+ - ["s", "⠠⠎"]
+ - ["t", "⠠⠞"]
+ - ["u", "⠠⠥"]
+ - ["v", "⠠⠧"]
+ - ["w", "⠠⠺"]
+ - ["x", "⠠⠭"]
+ - ["y", "⠠⠽"]
+ - ["z", "⠠⠵"]
+ - ["{", "⠘⠪"]
+ - ["|", "⠘⠸"]
+ - ["}", "⠘⠕"]
+ - ["~", "⠘⠠"]
+ - ["€", "⠘⠑"]
+ - ["‚", "⠈"]
+ - ["ƒ", "⠘⠋"]
+ - ["„", "⠶"]
+ - ["…", "⠄⠄⠄"]
+ - ["‰", "⠚⠴⠴"]
+ - ["Š", "⠨⠐⠎"]
+ - ["‹", "⠈"]
+ - ["Œ", "⠨⠕⠑"]
+ - ["Ž", "⠨⠐⠵"]
+ - ["‘", "⠈"]
+ - ["’", "⠈"]
+ - ["“", "⠶"]
+ - ["”", "⠶"]
+ - ["•", "⠘⠄"]
+ - ["–", "⠤⠤"]
+ - ["—", "⠤⠤"]
+ - ["˜", "⠘⠠"]
+ - ["™", "⠘⠞"]
+ - ["š", "⠐⠎"]
+ - ["›", "⠈"]
+ - ["œ", "⠕⠑"]
+ - ["ž", "⠐⠵"]
+ - ["Ÿ", "⠨⠐⠽"]
+ - ["¡", "⠠⠲"]
+ - ["¢", "⠘⠒"]
+ - ["£", "⠘⠇"]
+ - ["¥", "⠘⠽"]
+ - ["§", "⠬"]
+ - ["©", "⠘⠉"]
+ - ["«", "⠶"]
+ - ["®", "⠘⠗"]
+ - ["°", "⠈⠴"]
+ - ["±", "⠘⠖⠤"]
+ - ["²", "⠼⠬⠃"]
+ - ["³", "⠼⠬⠉"]
+ - ["´", "⠈"]
+ - ["µ", "⠐⠍"]
+ - ["¹", "⠼⠬⠁"]
+ - ["»", "⠶"]
+ - ["¼", "⠼⠁⠌⠙"]
+ - ["½", "⠼⠁⠌⠃"]
+ - ["¾", "⠼⠉⠌⠙"]
+ - ["À", "⠨⠐⠁"]
+ - ["Á", "⠨⠐⠁"]
+ - ["Â", "⠨⠐⠁"]
+ - ["Ã", "⠨⠐⠁"]
+ - ["Ä", "⠨⠠⠜"]
+ - ["Å", "⠨⠠⠡"]
+ - ["Æ", "⠨⠠⠜"]
+ - ["Ç", "⠨⠐⠉"]
+ - ["È", "⠨⠐⠑"]
+ - ["É", "⠨⠐⠑"]
+ - ["Ê", "⠨⠐⠑"]
+ - ["Ë", "⠨⠐⠑"]
+ - ["Ì", "⠨⠐⠊"]
+ - ["Í", "⠨⠐⠊"]
+ - ["Î", "⠨⠐⠊"]
+ - ["Ï", "⠨⠐⠊"]
+ - ["Ð", "⠨⠐⠙"]
+ - ["Ñ", "⠨⠐⠝"]
+ - ["Ò", "⠨⠐⠕"]
+ - ["Ó", "⠨⠐⠕"]
+ - ["Ô", "⠨⠐⠕"]
+ - ["Õ", "⠨⠐⠕"]
+ - ["Ö", "⠨⠠⠪"]
+ - ["×", "⠘⠄"]
+ - ["Ø", "⠨⠠⠪"]
+ - ["Ù", "⠨⠐⠥"]
+ - ["Ú", "⠨⠐⠥"]
+ - ["Û", "⠨⠐⠥"]
+ - ["Ü", "⠨⠠⠳"]
+ - ["Ý", "⠨⠐⠽"]
+ - ["Þ", "⠨⠐⠞"]
+ - ["ß", "⠎⠎"]
+ - ["à", "⠐⠁"]
+ - ["á", "⠐⠁"]
+ - ["â", "⠐⠁"]
+ - ["ã", "⠐⠁"]
+ - ["ä", "⠠⠜"]
+ - ["å", "⠠⠡"]
+ - ["æ", "⠠⠜"]
+ - ["ç", "⠐⠉"]
+ - ["è", "⠐⠑"]
+ - ["é", "⠐⠑"]
+ - ["ê", "⠐⠑"]
+ - ["ë", "⠐⠑"]
+ - ["ì", "⠐⠊"]
+ - ["í", "⠐⠊"]
+ - ["î", "⠐⠊"]
+ - ["ï", "⠐⠊"]
+ - ["ð", "⠐⠙"]
+ - ["ñ", "⠐⠝"]
+ - ["ò", "⠐⠕"]
+ - ["ó", "⠐⠕"]
+ - ["ô", "⠐⠕"]
+ - ["õ", "⠐⠕"]
+ - ["ö", "⠠⠪"]
+ - ["÷", "⠘⠲"]
+ - ["ø", "⠠⠪"]
+ - ["ù", "⠐⠥"]
+ - ["ú", "⠐⠥"]
+ - ["û", "⠐⠥"]
+ - ["ü", "⠠⠳"]
+ - ["ý", "⠐⠽"]
+ - ["þ", "⠐⠞"]
+ - ["ÿ", "⠐⠽"]
+
+# Latin Extended-A
+
+ - ["\u0100\u0101", "⠐⠁⠐⠁"]
+ - ["\u0102\u0103", "⠐⠁⠐⠁"]
+ - ["\u0104\u0105", "⠐⠁⠐⠁"]
+ - ["\u0106\u0107", "⠐⠉⠐⠉"]
+ - ["\u0108\u0109", "⠐⠉⠐⠉"]
+ - ["\u010a\u010b", "⠐⠉⠐⠉"]
+ - ["\u010c\u010d", "⠐⠉⠐⠉"]
+ - ["\u010e\u010f", "⠐⠙⠐⠙"]
+ - ["\u0110\u0111", "⠐⠙⠐⠙"]
+ - ["\u0112\u0113", "⠐⠑⠐⠑"]
+ - ["\u0114\u0115", "⠐⠑⠐⠑"]
+ - ["\u0116\u0117", "⠐⠑⠐⠑"]
+ - ["\u0118\u0119", "⠐⠑⠐⠑"]
+ - ["\u011a\u011b", "⠐⠑⠐⠑"]
+ - ["\u011c\u011d", "⠐⠛⠐⠛"]
+ - ["\u011e\u011f", "⠐⠛⠐⠛"]
+ - ["\u0120\u0121", "⠐⠛⠐⠛"]
+ - ["\u0122\u0123", "⠐⠛⠐⠛"]
+ - ["\u0124\u0125", "⠐⠓⠐⠓"]
+ - ["\u0126\u0127", "⠐⠓⠐⠓"]
+ - ["\u0128\u0129", "⠐⠊⠐⠊"]
+ - ["\u012a\u012b", "⠐⠊⠐⠊"]
+ - ["\u012c\u012d", "⠐⠊⠐⠊"]
+ - ["\u012e\u012f", "⠐⠊⠐⠊"]
+ - ["\u0130\u0131", "⠐⠊⠐⠊"]
+ - ["\u0132\u0133", "⠊⠚⠊⠚"]
+ - ["\u0134\u0135", "⠐⠚⠐⠚"]
+ - ["\u0136\u0137", "⠐⠅⠐⠅"]
+ - ["\u0138", "⠐⠟"]
+ - ["\u0139\u013a", "⠐⠇⠐⠇"]
+ - ["\u013b\u013c", "⠐⠇⠐⠇"]
+ - ["\u013d\u013e", "⠐⠇⠐⠇"]
+ - ["\u013f\u0140", "⠐⠇⠐⠇"]
+ - ["\u0141\u0142", "⠐⠇⠐⠇"]
+ - ["\u0143\u0144", "⠐⠝⠐⠝"]
+ - ["\u0145\u0146", "⠐⠝⠐⠝"]
+ - ["\u0147\u0148", "⠐⠝⠐⠝"]
+ - ["\u0149", "⠈⠝"]
+ - ["\u014a\u014b", "⠐⠝⠐⠝"]
+ - ["\u014c\u014d", "⠐⠕⠐⠕"]
+ - ["\u014e\u014f", "⠐⠕⠐⠕"]
+ - ["\u0150\u0151", "⠐⠕⠐⠕"]
+ - ["\u0152\u0153", "⠕⠑⠕⠑"]
+ - ["\u0154\u0155", "⠐⠗⠐⠗"]
+ - ["\u0156\u0157", "⠐⠗⠐⠗"]
+ - ["\u0158\u0159", "⠐⠗⠐⠗"]
+ - ["\u015a\u015b", "⠐⠎⠐⠎"]
+ - ["\u015c\u015d", "⠐⠎⠐⠎"]
+ - ["\u015e\u015f", "⠐⠎⠐⠎"]
+ - ["\u0160\u0161", "⠐⠎⠐⠎"]
+ - ["\u0162\u0163", "⠐⠞⠐⠞"]
+ - ["\u0164\u0165", "⠐⠞⠐⠞"]
+ - ["\u0166\u0167", "⠐⠞⠐⠞"]
+ - ["\u0168\u0169", "⠐⠥⠐⠥"]
+ - ["\u016a\u016b", "⠐⠥⠐⠥"]
+ - ["\u016c\u016d", "⠐⠥⠐⠥"]
+ - ["\u016e\u016f", "⠐⠥⠐⠥"]
+ - ["\u0170\u0171", "⠐⠥⠐⠥"]
+ - ["\u0172\u0173", "⠐⠥⠐⠥"]
+ - ["\u0174\u0175", "⠐⠺⠐⠺"]
+ - ["\u0176\u0177", "⠐⠽⠐⠽"]
+ - ["\u0178\u00ff", "⠐⠽⠐⠽"]
+ - ["\u0179\u017a", "⠐⠵⠐⠵"]
+ - ["\u017b\u017c", "⠐⠵⠐⠵"]
+ - ["\u017d\u017e", "⠐⠵⠐⠵"]
+ - ["\u017f", "⠐⠎"]
+
+# Latin Extended-B
+
+ - ["\u0192", "⠘⠋"]
+ - ["\u02dc", "⠘⠠"]
+
+# Greek letters (with dot 5 as prefix)
+
+ - ["\u0386\u03ac", "⠐⠁⠐⠁"]
+ - ["\u0388\u03ad", "⠐⠑⠐⠑"]
+ - ["\u0389\u03ae", "⠐⠱⠐⠱"]
+ - ["\u038a\u03af", "⠐⠊⠐⠊"]
+ - ["\u038c\u03cc", "⠐⠕⠐⠕"]
+ - ["\u038e\u03cd", "⠐⠥⠐⠥"]
+ - ["\u038f\u03ce", "⠐⠺⠐⠺"]
+ - ["\u0391\u03b1", "⠐⠁⠐⠁"]
+ - ["\u0392\u03b2", "⠐⠃⠐⠃"]
+ - ["\u0393\u03b3", "⠐⠛⠐⠛"]
+ - ["\u0394\u03b4", "⠐⠙⠐⠙"]
+ - ["\u0395\u03b5", "⠐⠑⠐⠑"]
+ - ["\u0396\u03b6", "⠐⠵⠐⠵"]
+ - ["\u0397\u03b7", "⠐⠱⠐⠱"]
+ - ["\u0398\u03b8", "⠐⠹⠐⠹"]
+ - ["\u0399\u03b9", "⠐⠊⠐⠊"]
+ - ["\u039a\u03ba", "⠐⠅⠐⠅"]
+ - ["\u039b\u03bb", "⠐⠇⠐⠇"]
+ - ["\u039c\u03bc", "⠐⠍⠐⠍"]
+ - ["\u039d\u03bd", "⠐⠝⠐⠝"]
+ - ["\u039e\u03be", "⠐⠭⠐⠭"]
+ - ["\u039f\u03bf", "⠐⠕⠐⠕"]
+ - ["\u03a0\u03c0", "⠐⠏⠐⠏"]
+ - ["\u03a1\u03c1", "⠐⠗⠐⠗"]
+ - ["\u03a3\u03c3", "⠐⠎⠐⠎"]
+ - ["\u03a4\u03c4", "⠐⠞⠐⠞"]
+ - ["\u03a5\u03c5", "⠐⠥⠐⠥"]
+ - ["\u03a6\u03c6", "⠐⠋⠐⠋"]
+ - ["\u03a7\u03c7", "⠐⠓⠐⠓"]
+ - ["\u03a8\u03c8", "⠐⠽⠐⠽"]
+ - ["\u03a9\u03c9", "⠐⠺⠐⠺"]
+
+# Punctuation and bullits
+
+ - ["\u2000", " "]
+ - ["\u2001", " "]
+ - ["\u2002", " "]
+ - ["\u2003", " "]
+ - ["\u2004", " "]
+ - ["\u2005", " "]
+ - ["\u2006", " "]
+ - ["\u2007", " "]
+ - ["\u2008", " "]
+ - ["\u2009", " "]
+ - ["\u200a", " "]
+ - ["\u2010", "⠤"]
+ - ["\u2011", "⠤"]
+ - ["\u2012", "⠤"]
+ - ["\u2013", "⠤⠤"]
+ - ["\u2014", "⠤⠤"]
+ - ["\u2018", "⠈"]
+ - ["\u2019", "⠈"]
+ - ["\u201a", "⠈"]
+ - ["\u201b", "⠈"]
+ - ["\u201c", "⠶"]
+ - ["\u201d", "⠶"]
+ - ["\u201e", "⠶"]
+ - ["\u201f", "⠶"]
+ - ["\u2023", "⠘⠄"]
+ - ["\u2026", "⠄⠄⠄"]
+ - ["\u202f", " "]
+ - ["\u2030", "⠚⠴⠴"]
+ - ["\u2039", "⠈"]
+ - ["\u203a", "⠈"]
+ - ["\u203c", "⠖⠖"]
+ - ["\u203d", "⠢⠖"]
+ - ["\u2043", "⠘⠄"]
+ - ["\u2047", "⠢⠢"]
+ - ["\u2048", "⠢⠖"]
+ - ["\u2049", "⠖⠢"]
+ - ["\u204c", "⠘⠄"]
+ - ["\u204d", "⠘⠄"]
+ - ["\u20ac", "⠘⠑"]
+ - ["\u2122", "⠘⠞"]
+
+# Arrows (only a few in 6 dots)
+
+ - ["\u2190", "⠘⠺"]
+ - ["\u2191", "⠘⠷"]
+ - ["\u2192", "⠘⠗"]
+ - ["\u2193", "⠘⠟"]
+
+# Geometrical shapes
+
+ - ["\u25e6", "⠘⠄"]
+
+# Pangram
+
+ - - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ - "⠠⠟⠥⠊⠠⠵⠹⠇⠞⠁⠛⠱⠫ ⠎⠏⠊⠵⠑ ⠚⠭⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠹⠂ ⠍⠣⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠣ ⠠⠺⠁⠇⠞⠓⠱ ⠎⠏⠊⠇⠇⠑⠹ ⠏ ⠠⠭⠽⠇⠕⠋⠕⠝⠄"
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⠬ ⠰⠭⠙⠰ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⠬ ⠰⠭⠙⠰ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⠬ ⠰⠭⠙⠰ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⠣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⠬ ⠰⠭⠙⠰ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⠬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# Caps and mixed case
+
+ - ["Foobar", "⠋⠕⠕⠃⠁⠗"]
+ - ["FOOBAR", "⠸⠋⠕⠕⠃⠁⠗"]
+ - ["FOObar", "⠸⠋⠕⠕⠠⠃⠁⠗"]
+ - ["FOO-barfOObar", "⠸⠋⠕⠕⠤⠃⠁⠗⠋⠸⠕⠕⠠⠃⠁⠗"]
+
+# Percent and permille
+
+ - ["1%", "⠼⠁ ⠚⠴"]
+
+# Extra digits
+
+ - ["1\u00bc + 1\u00bd = 2\u00be", "⠼⠁⠼⠁⠌⠙ ⠖⠼⠁⠼⠁⠌⠃ ⠶⠼⠃⠼⠉⠌⠙"]
+
+# Dashes
+
+ - ["\u2014 \u0096 \u0097 \u00ad", "⠤⠤ ⠤⠤ ⠤⠤ ⠤⠤"]
+
+# Quotes
+
+ - ["\u201e \u0084 \u201c \u0093 \u201d \u0094 \u00ab \u00bb", "⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶ ⠶"]
+
+# Apostrophes
+
+ - ["` \u201a \u0082 \u2039 \u008b \u2018 \u0091 \u2019 \u0092 \u203a \u009b \u00b4", "⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈ ⠈"]
+
+# Times vs. bullit
+
+ - ["\u2022Bullit", "⠘⠄⠃⠥⠇⠇⠊⠞"]
+ - ["2 \u00d7 2 = 4", "⠼⠃ ⠄⠼⠃ ⠶⠼⠙"]
+
+# Section sign
+ - ["§ 3", "⠬⠼⠉"]
+ - ["§ 3:", "⠬⠼⠉⠒"]
+
+# Numbers and punctuation
+
+ - ["1,2", "⠼⠁⠂⠃"]
+ - ["123.456", "⠼⠁⠃⠉⠄⠙⠑⠋"]
+ - ["3-4", "⠼⠉⠤⠼⠙"]
+ - ["56-", "⠼⠑⠋⠤"]
+ - ["1/2", "⠼⠁⠌⠃"]
+ - ["12:34", "⠼⠁⠃⠒⠉⠙"]
+ - ["2^10", "⠼⠃⠘⠬⠁⠚"]
+ - ["2\u00d72", "⠼⠃⠘⠄⠼⠃"]
+
+ - ["\"quotes\"", "⠶⠠⠟⠥⠕⠳⠎⠶"]
+ - ["-dashes-", "⠤⠙⠁⠎⠓⠑⠎⠤"]
+ - [":-) :-(", "⠒⠤⠠⠴ ⠒⠤⠠⠦"]
+ - [";-) ;-(", "⠆⠤⠠⠴ ⠆⠤⠠⠦"]
+ - ["---", "⠤⠤⠤"]
+ - ["-", "⠤⠤"]
+ - [" -", " ⠤⠤"]
+ - [" a-", " ⠠⠁⠤"]
+ - [" - ", " ⠤⠤ "]
+ - [" -a-", " ⠤⠠⠁⠤"]
+ - [" ", " "]
+ - ["(parentheses)", "⠦⠏⠁⠗⠣⠞⠓⠑⠎⠑⠎⠴"]
+ - ["J) j) %) ') \u2030) \u0089) \u201a) \u0082) \u2039) \u009b) \u2018) \u0091) \u2019) \u0092) \u203a) \u009b)", "⠨⠠⠚⠠⠴ ⠠⠚⠠⠴ ⠚⠴⠠⠴ ⠈⠠⠴ ⠚⠴⠴⠠⠴ ⠚⠴⠴⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴ ⠈⠠⠴"]
+
+# Exclamation
+
+ - ["\u00a1Que lastima!", "⠠⠲⠠⠟⠥⠑ ⠇⠁⠵⠊⠍⠁⠖"]
+
+# Digits and letters
+
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["1.,-a", "⠼⠁⠄⠂⠤⠠⠁"]
+
+# URLs emails and file names
+
+ - ["$at", "⠘⠲⠁⠞"]
+ - ["\u005c\u005cat\u005c\u005cbliver", "⠘⠡⠁⠞⠘⠡⠃⠇⠊⠧⠑⠗"]
+ - ["at@bliver.og", "⠁⠞⠘⠁⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["http://at.bliver.og", "⠓⠞⠞⠏⠒⠌⠌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.og", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛"]
+ - ["www.at.bliver.com", "⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍"]
+ - ["www.a.b.c", "⠠⠺⠠⠺⠠⠺⠄⠠⠁⠄⠠⠃⠄⠠⠉"]
+ - ["test.txt", "⠳⠵⠄⠞⠠⠭⠞"]
+
+# Word contractions
+
+ - ["At", "⠁"]
+ - ["at", "⠁"]
+ - ["Aldrig", "⠁⠔"]
+ - ["aldrig", "⠁⠔"]
+ - ["aig", "⠁⠊⠛"]
+ - ["Alle", "⠁⠑"]
+ - ["alle", "⠁⠑"]
+ - ["ae", "⠠⠁⠑"]
+ - ["Allerede", "⠁⠇⠗"]
+ - ["allerede", "⠁⠇⠗"]
+ - ["alr", "⠠⠁⠇⠗"]
+ - ["Alligevel", "⠁⠇⠧"]
+ - ["alligevel", "⠁⠇⠧"]
+ - ["alv", "⠠⠁⠇⠧"]
+ - ["Altid", "⠁⠞⠙"]
+ - ["altid", "⠁⠞⠙"]
+ - ["atd", "⠠⠁⠞⠙"]
+ - ["Altså", "⠁⠡"]
+ - ["altså", "⠁⠡"]
+ - ["aå", "⠠⠁⠡"]
+ - ["Bliver", "⠃"]
+ - ["bliver", "⠃"]
+ - ["Og", "⠉"]
+ - ["og", "⠉"]
+ - ["Deres", "⠲"]
+ - ["deres", "⠲"]
+ - ["Du", "⠙"]
+ - ["du", "⠙"]
+ - ["Eller", "⠑"]
+ - ["eller", "⠑"]
+ - ["For", "⠋"]
+ - ["for", "⠋"]
+ - ["Gør", "⠛"]
+ - ["gør", "⠛"]
+ - ["Har", "⠓"]
+ - ["har", "⠓"]
+ - ["Jeg", "⠚"]
+ - ["jeg", "⠚"]
+ - ["Kan", "⠅"]
+ - ["kan", "⠅"]
+ - ["Lige", "⠇"]
+ - ["lige", "⠇"]
+ - ["Med", "⠍"]
+ - ["med", "⠍"]
+ - ["Når", "⠝"]
+ - ["når", "⠝"]
+ - ["Op", "⠕"]
+ - ["op", "⠕"]
+ - ["På", "⠏"]
+ - ["på", "⠏"]
+ - ["Under", "⠟"]
+ - ["under", "⠟"]
+ - ["Rigtig", "⠗"]
+ - ["rigtig", "⠗"]
+ - ["Som", "⠎"]
+ - ["som", "⠎"]
+ - ["Til", "⠞"]
+ - ["til", "⠞"]
+ - ["Hun", "⠥"]
+ - ["hun", "⠥"]
+ - ["Ved", "⠧"]
+ - ["ved", "⠧"]
+ - ["Hvad", "⠺"]
+ - ["hvad", "⠺"]
+ - ["Over", "⠭"]
+ - ["over", "⠭"]
+ - ["Han", "⠽"]
+ - ["han", "⠽"]
+ - ["Efter", "⠵"]
+ - ["efter", "⠵"]
+ - ["Være", "⠜"]
+ - ["være", "⠜"]
+ - ["Før", "⠪"]
+ - ["før", "⠪"]
+ - ["Så", "⠡"]
+ - ["så", "⠡"]
+ - ["Den", "⠯"]
+ - ["den", "⠯"]
+ - ["Der", "⠾"]
+ - ["der", "⠾"]
+ - ["Det", "⠮"]
+ - ["det", "⠮"]
+ - ["De", "⠹"]
+ - ["de", "⠹"]
+ - ["En", "⠣"]
+ - ["en", "⠣"]
+ - ["Er", "⠱"]
+ - ["er", "⠱"]
+ - ["Et", "⠬"]
+ - ["et", "⠬"]
+ - ["Gennem", "⠻"]
+ - ["gennem", "⠻"]
+ - ["Hvor", "⠌"]
+ - ["hvor", "⠌"]
+ - ["Men", "⠩"]
+ - ["men", "⠩"]
+ - ["Ned", "⠫"]
+ - ["ned", "⠫"]
+ - ["Ret", "⠷"]
+ - ["ret", "⠷"]
+ - ["Skal", "⠿"]
+ - ["skal", "⠿"]
+ - ["Te", "⠳"]
+ - ["te", "⠳"]
+ - ["Ve", "⠼"]
+ - ["ve", "⠼"]
+
+# Partword/nocross
+
+ - ["Denne", "⠯⠫"]
+ - ["denne", "⠯⠫"]
+ - ["Mændene", "⠍⠜⠝⠹⠫"]
+ - ["mændene", "⠍⠜⠝⠹⠫"]
+ - ["Derhos", "⠾⠓⠕⠎"]
+ - ["derhos", "⠾⠓⠕⠎"]
+ - ["Hunderace", "⠓⠥⠝⠹⠗⠁⠉⠑"]
+ - ["hunderace", "⠓⠥⠝⠹⠗⠁⠉⠑"]
+ - ["Dette", "⠮⠳"]
+ - ["dette", "⠮⠳"]
+ - ["Detalje", "⠹⠞⠁⠇⠚⠑"]
+ - ["detalje", "⠹⠞⠁⠇⠚⠑"]
+
+# Nocross multiple cells
+
+ - ["Endda", "⠑⠟⠙⠁"]
+ - ["endda", "⠑⠟⠙⠁"]
+ - ["Morgendag", "⠍⠭⠛⠣⠙⠁⠛"]
+ - ["morgendag", "⠍⠭⠛⠣⠙⠁⠛"]
+ - ["Gendanne", "⠛⠣⠙⠁⠝⠫"]
+ - ["gendanne", "⠛⠣⠙⠁⠝⠫"]
+ - ["Generelt", "⠻⠫⠷⠇⠞"]
+ - ["generelt", "⠻⠫⠷⠇⠞"]
+
+ - ["Fra!", "⠋⠗⠁⠖"]
+ - ["fra!", "⠋⠗⠁⠖"]
+ - ["!Fra", "⠖⠋⠗⠁"]
+ - ["!fra", "⠖⠋⠗⠁"]
+ - ["'Af", "⠈⠁⠋"]
+ - ["'af", "⠈⠁⠋"]
+
+
+# No single cell contractions before or after dashes
+
+ - ["at-bliver", "⠁⠞⠤⠃"]
+ - ["d-d-du", "⠠⠙⠤⠠⠙⠤⠙⠥"]
+
+# Combinations with slashes and other punctuation signs
+
+ - ["Han/hun", "⠽⠌⠥"]
+ - ["han/hun", "⠽⠌⠥"]
+ - ["Over/under", "⠭⠌⠟"]
+ - ["over/under", "⠭⠌⠟"]
+ - ["Til/fra", "⠞⠌⠖"]
+ - ["til/fra", "⠞⠌⠖"]
+
+# Combinations which require letsign
+
+ - ["1st", "⠼⠁⠠⠎⠞"]
+ - ["2nd", "⠼⠃⠠⠝⠙"]
+ - ["1A", "⠼⠁⠨⠠⠁", {xfail: true}] # unclear spec
+ - ["1a", "⠼⠁⠠⠁"]
+ - ["2B", "⠼⠃⠨⠠⠃", {xfail: true}] # unclear spec
+ - ["2b", "⠼⠃⠠⠃"]
+
+# Multi-pass tests
+
+ - ["~x |z", "⠘⠠⠠⠭ ⠘⠸⠠⠵"]
+ - ["~X |Z", "⠘⠠⠨⠠⠭ ⠘⠸⠨⠠⠵"]
+ - ["5É", "⠼⠑⠨⠐⠑"]
+
+#############
+### 8-DOT ###
+#############
+
+# -------
+# Grade 0
+# -------
+
+table: {language: da, grade: 0, dots: 8, version: 1993, __assert-match: da-dk-g08_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - [' ', ' ']
+ - ['"', '⠶']
+ - ['$', '⣲']
+ - ['%', '⣚']
+ - ['&', '⢯']
+ - ['''', '⠈']
+ - ['(', '⢦']
+ - [')', '⢴']
+ - ['*', '⠔']
+ - ['+', '⢖']
+ - [',', '⠂']
+ - ['-', '⢤']
+ - ['.', '⠄']
+ - ['/', '⢌']
+ - ['0', '⢚']
+ - ['1', '⢁']
+ - ['2', '⢃']
+ - ['3', '⢉']
+ - ['4', '⢙']
+ - ['5', '⢑']
+ - ['6', '⢋']
+ - ['7', '⢛']
+ - ['8', '⢓']
+ - ['9', '⢊']
+ - [':', '⠒']
+ - [';', '⠆']
+ - ['<', '⢔']
+ - ['=', '⢶']
+ - ['>', '⡢']
+ - ['?', '⠢']
+ - ['@', '⣈']
+ - ['A', '⡁']
+ - ['B', '⡃']
+ - ['C', '⡉']
+ - ['D', '⡙']
+ - ['E', '⡑']
+ - ['F', '⡋']
+ - ['G', '⡛']
+ - ['H', '⡓']
+ - ['I', '⡊']
+ - ['J', '⡚']
+ - ['K', '⡅']
+ - ['L', '⡇']
+ - ['M', '⡍']
+ - ['N', '⡝']
+ - ['O', '⡕']
+ - ['P', '⡏']
+ - ['Q', '⡟']
+ - ['R', '⡗']
+ - ['S', '⡎']
+ - ['T', '⡞']
+ - ['U', '⡥']
+ - ['V', '⡧']
+ - ['W', '⡺']
+ - ['X', '⡭']
+ - ['Y', '⡽']
+ - ['Z', '⡵']
+ - ['[', '⣦']
+ - ['\\', '⡌']
+ - [']', '⣴']
+ - ['^', '⢏']
+ - ['_', '⣤']
+ - ['`', '⠐']
+ - ['a', '⠁']
+ - ['b', '⠃']
+ - ['c', '⠉']
+ - ['d', '⠙']
+ - ['e', '⠑']
+ - ['f', '⠋']
+ - ['g', '⠛']
+ - ['h', '⠓']
+ - ['i', '⠊']
+ - ['j', '⠚']
+ - ['k', '⠅']
+ - ['l', '⠇']
+ - ['m', '⠍']
+ - ['n', '⠝']
+ - ['o', '⠕']
+ - ['p', '⠏']
+ - ['q', '⠟']
+ - ['r', '⠗']
+ - ['s', '⠎']
+ - ['t', '⠞']
+ - ['u', '⠥']
+ - ['v', '⠧']
+ - ['w', '⠺']
+ - ['x', '⠭']
+ - ['y', '⠽']
+ - ['z', '⠵']
+ - ['{', '⣧']
+ - ['|', '⢸']
+ - ['}', '⣼']
+ - ['~', '⡨']
+ - ['€', '⣑']
+ - ['‚', '⡘']
+ - ['ƒ', '⢐']
+ - ['„', '⣆']
+ - ['…', '⠠']
+ - ['†', '⡖']
+ - ['‡', '⣖']
+ - ['ˆ', '⣰']
+ - ['‰', '⣺']
+ - ['Š', '⣎']
+ - ['‹', '⠸']
+ - ['Œ', '⣕']
+ - ['Ž', '⡬']
+ - ['‘', '⡈']
+ - ['’', '⢈']
+ - ['“', '⡆']
+ - ['”', '⢰']
+ - ['•', '⡄']
+ - ['–', '⠤']
+ - ['—', '⡤']
+ - ['˜', '⠨']
+ - ['™', '⣞']
+ - ['š', '⢎']
+ - ['›', '⡸']
+ - ['œ', '⢕']
+ - ['ž', '⠬']
+ - ['Ÿ', '⣾']
+ - [' ', '⢞']
+ - ['¡', '⠲']
+ - ['¢', '⣒']
+ - ['£', '⢇']
+ - ['¤', '⡦']
+ - ['¥', '⡠']
+ - ['¦', '⣌']
+ - ['§', '⣐']
+ - ['¨', '⠰']
+ - ['©', '⣭']
+ - ['ª', '⣮']
+ - ['«', '⡐']
+ - ['¬', '⡼']
+ - ['', '⣄']
+ - ['®', '⣗']
+ - ['¯', '⡶']
+ - ['°', '⠴']
+ - ['±', '⢟']
+ - ['²', '⢆']
+ - ['³', '⢒']
+ - ['´', '⢨']
+ - ['µ', '⠦']
+ - ['¶', '⢿']
+ - ['·', '⢄']
+ - ['¸', '⣨']
+ - ['¹', '⢂']
+ - ['º', '⣿']
+ - ['»', '⡰']
+ - ['¼', '⢝']
+ - ['½', '⢘']
+ - ['¾', '⠼']
+ - ['À', '⡷']
+ - ['Á', '⣷']
+ - ['Â', '⣡']
+ - ['Ã', '⣩']
+ - ['Ä', '⣜']
+ - ['Å', '⡡']
+ - ['Æ', '⡜']
+ - ['Ç', '⡯']
+ - ['È', '⡮']
+ - ['É', '⡿']
+ - ['Ê', '⡣']
+ - ['Ë', '⡫']
+ - ['Ì', '⣱']
+ - ['Í', '⣣']
+ - ['Î', '⡩']
+ - ['Ï', '⡻']
+ - ['Ð', '⣽']
+ - ['Ñ', '⣻']
+ - ['Ò', '⣫']
+ - ['Ó', '⣬']
+ - ['Ô', '⡹']
+ - ['Õ', '⣹']
+ - ['Ö', '⣪']
+ - ['×', '⢭']
+ - ['Ø', '⡪']
+ - ['Ù', '⡾']
+ - ['Ú', '⣳']
+ - ['Û', '⡱']
+ - ['Ü', '⡳']
+ - ['Ý', '⣍']
+ - ['Þ', '⣅']
+ - ['ß', '⢮']
+ - ['à', '⠷']
+ - ['á', '⢷']
+ - ['â', '⢡']
+ - ['ã', '⢩']
+ - ['ä', '⢜']
+ - ['å', '⠡']
+ - ['æ', '⠜']
+ - ['ç', '⠯']
+ - ['è', '⠮']
+ - ['é', '⠿']
+ - ['ê', '⠣']
+ - ['ë', '⠫']
+ - ['ì', '⢱']
+ - ['í', '⢣']
+ - ['î', '⠩']
+ - ['ï', '⠻']
+ - ['ð', '⢽']
+ - ['ñ', '⢻']
+ - ['ò', '⢫']
+ - ['ó', '⢬']
+ - ['ô', '⠹']
+ - ['õ', '⢹']
+ - ['ö', '⢪']
+ - ['÷', '⢲']
+ - ['ø', '⠪']
+ - ['ù', '⠾']
+ - ['ú', '⢳']
+ - ['û', '⠱']
+ - ['ü', '⠳']
+ - ['ý', '⢍']
+ - ['þ', '⢅']
+ - ['ÿ', '⢾']
+
+# Pangram
+
+ - - 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon.'
+ - '⡟⠥⠊⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍⠑⠙ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⡺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏⠡ ⠭⠽⠇⠕⠋⠕⠝⠄'
+
+# -------
+# Grade 1
+# -------
+
+table: {language: da, grade: 1, dots: 8, version: 1993, __assert-match: da-dk-g18_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - [' ', ' ']
+ - ['"', '⠶']
+ - ['$', '⣲']
+ - ['%', '⣚']
+ - ['&', '⢯']
+ - ['''', '⠈']
+ - ['(', '⢦']
+ - [')', '⢴']
+ - ['*', '⠔']
+ - ['+', '⢖']
+ - [',', '⠂']
+ - ['-', '⢤']
+ - ['.', '⠄']
+ - ['/', '⢌']
+ - ['0', '⢚']
+ - ['1', '⢁']
+ - ['2', '⢃']
+ - ['3', '⢉']
+ - ['4', '⢙']
+ - ['5', '⢑']
+ - ['6', '⢋']
+ - ['7', '⢛']
+ - ['8', '⢓']
+ - ['9', '⢊']
+ - [':', '⠒']
+ - [';', '⠆']
+ - ['<', '⢔']
+ - ['=', '⢶']
+ - ['>', '⡢']
+ - ['?', '⠢']
+ - ['@', '⣈']
+ - ['A', '⡁']
+ - ['B', '⡃']
+ - ['C', '⡉']
+ - ['D', '⡙']
+ - ['E', '⡑']
+ - ['F', '⡋']
+ - ['G', '⡛']
+ - ['H', '⡓']
+ - ['I', '⡊']
+ - ['J', '⡚']
+ - ['K', '⡅']
+ - ['L', '⡇']
+ - ['M', '⡍']
+ - ['N', '⡝']
+ - ['O', '⡕']
+ - ['P', '⡏']
+ - ['Q', '⡟']
+ - ['R', '⡗']
+ - ['S', '⡎']
+ - ['T', '⡞']
+ - ['U', '⡥']
+ - ['V', '⡧']
+ - ['W', '⡺']
+ - ['X', '⡭']
+ - ['Y', '⡽']
+ - ['Z', '⡵']
+ - ['[', '⣦']
+ - ['\\', '⡌']
+ - [']', '⣴']
+ - ['^', '⢏']
+ - ['_', '⣤']
+ - ['`', '⠐']
+ - ['a', '⠁']
+ - ['b', '⠃']
+ - ['c', '⠉']
+ - ['d', '⠙']
+ - ['e', '⠑']
+ - ['f', '⠋']
+ - ['g', '⠛']
+ - ['h', '⠓']
+ - ['i', '⠊']
+ - ['j', '⠚']
+ - ['k', '⠅']
+ - ['l', '⠇']
+ - ['m', '⠍']
+ - ['n', '⠝']
+ - ['o', '⠕']
+ - ['p', '⠏']
+ - ['q', '⠟']
+ - ['r', '⠗']
+ - ['s', '⠎']
+ - ['t', '⠞']
+ - ['u', '⠥']
+ - ['v', '⠧']
+ - ['w', '⠺']
+ - ['x', '⠭']
+ - ['y', '⠽']
+ - ['z', '⠵']
+ - ['{', '⣧']
+ - ['|', '⢸']
+ - ['}', '⣼']
+ - ['~', '⡨']
+ - ['€', '⣑']
+ - ['‚', '⡘']
+ - ['ƒ', '⢐']
+ - ['„', '⣆']
+ - ['…', '⠠⠄⠄⠄']
+ - ['†', '⡖']
+ - ['‡', '⣖']
+ - ['ˆ', '⣰']
+ - ['‰', '⣺']
+ - ['Š', '⣎']
+ - ['‹', '⠸']
+ - ['Œ', '⣕']
+ - ['Ž', '⡬']
+ - ['‘', '⡈']
+ - ['’', '⢈']
+ - ['“', '⡆']
+ - ['”', '⢰']
+ - ['•', '⡄']
+ - ['–', '⠠⠤']
+ - ['—', '⠠⡤']
+ - ['˜', '⠨']
+ - ['™', '⣞']
+ - ['š', '⢎']
+ - ['›', '⡸']
+ - ['œ', '⢕']
+ - ['ž', '⠬']
+ - ['Ÿ', '⣾']
+ - [' ', '⢞']
+ - ['¡', '⠲']
+ - ['¢', '⣒']
+ - ['£', '⢇']
+ - ['¤', '⡦']
+ - ['¥', '⡠']
+ - ['¦', '⣌']
+ - ['§', '⣐']
+ - ['¨', '⠰']
+ - ['©', '⣭']
+ - ['ª', '⣮']
+ - ['«', '⡐']
+ - ['¬', '⡼']
+ - ['', '⣄']
+ - ['®', '⣗']
+ - ['¯', '⡶']
+ - ['°', '⠴']
+ - ['±', '⢟']
+ - ['²', '⢆']
+ - ['³', '⢒']
+ - ['´', '⢨']
+ - ['µ', '⠦']
+ - ['¶', '⢿']
+ - ['·', '⢄']
+ - ['¸', '⣨']
+ - ['¹', '⢂']
+ - ['º', '⣿']
+ - ['»', '⡰']
+ - ['¼', '⢝']
+ - ['½', '⢘']
+ - ['¾', '⠼']
+ - ['À', '⡷']
+ - ['Á', '⣷']
+ - ['Â', '⣡']
+ - ['Ã', '⣩']
+ - ['Ä', '⣜']
+ - ['Å', '⡡']
+ - ['Æ', '⡜']
+ - ['Ç', '⡯']
+ - ['È', '⡮']
+ - ['É', '⡿']
+ - ['Ê', '⡣']
+ - ['Ë', '⡫']
+ - ['Ì', '⣱']
+ - ['Í', '⣣']
+ - ['Î', '⡩']
+ - ['Ï', '⡻']
+ - ['Ð', '⣽']
+ - ['Ñ', '⣻']
+ - ['Ò', '⣫']
+ - ['Ó', '⣬']
+ - ['Ô', '⡹']
+ - ['Õ', '⣹']
+ - ['Ö', '⣪']
+ - ['×', '⢭']
+ - ['Ø', '⡪']
+ - ['Ù', '⡾']
+ - ['Ú', '⣳']
+ - ['Û', '⡱']
+ - ['Ü', '⡳']
+ - ['Ý', '⣍']
+ - ['Þ', '⣅']
+ - ['ß', '⢮']
+ - ['à', '⠷']
+ - ['á', '⢷']
+ - ['â', '⢡']
+ - ['ã', '⢩']
+ - ['ä', '⢜']
+ - ['å', '⠡']
+ - ['æ', '⠜']
+ - ['ç', '⠯']
+ - ['è', '⠮']
+ - ['é', '⠿']
+ - ['ê', '⠣']
+ - ['ë', '⠫']
+ - ['ì', '⢱']
+ - ['í', '⢣']
+ - ['î', '⠩']
+ - ['ï', '⠻']
+ - ['ð', '⢽']
+ - ['ñ', '⢻']
+ - ['ò', '⢫']
+ - ['ó', '⢬']
+ - ['ô', '⠹']
+ - ['õ', '⢹']
+ - ['ö', '⢪']
+ - ['÷', '⢲']
+ - ['ø', '⠪']
+ - ['ù', '⠾']
+ - ['ú', '⢳']
+ - ['û', '⠱']
+ - ['ü', '⠳']
+ - ['ý', '⢍']
+ - ['þ', '⢅']
+ - ['ÿ', '⢾']
+
+# Misc Unicode chars
+# For each accented letter, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ['\x0100\x0101', '⠐⡁⠐⠁']
+ - ['\x0106\x0107', '⠐⡉⠐⠉']
+ - ['\x010e\x010f', '⠐⡙⠐⠙']
+ - ['\x0112\x0113', '⠐⡑⠐⠑']
+ - ['\x011c\x011d', '⠐⡛⠐⠛']
+ - ['\x0124\x0125', '⠐⡓⠐⠓']
+ - ['\x0128\x0129', '⠐⡊⠐⠊']
+ - ['\x0134\x0135', '⠐⡚⠐⠚']
+ - ['\x0136\x0137', '⠐⡅⠐⠅']
+ - ['\x0138', '⠐⠟']
+ - ['\x0139\x013a', '⠐⡇⠐⠇']
+ - ['\x0143\x0144', '⠐⡝⠐⠝']
+ - ['\x014c\x014d', '⠐⡕⠐⠕']
+ - ['\x0154\x0155', '⠐⡗⠐⠗']
+ - ['\x015a\x015b', '⠐⡎⠐⠎']
+ - ['\x0162\x0163', '⠐⡞⠐⠞']
+ - ['\x0168\x0169', '⠐⡥⠐⠥']
+ - ['\x0174\x0175', '⠐⡺⠐⠺']
+ - ['\x0176\x0177', '⠐⡽⠐⠽']
+ - ['\x0179\x017a', '⠐⡵⠐⠵']
+
+# Greek letters (with dots 458 as prefix)
+
+ - ['\x0386\x03ac', '⢘⠐⡁⢘⠐⠁']
+ - ['\x0388\x03ad', '⢘⠐⡑⢘⠐⠑']
+ - ['\x0389\x03ae', '⢘⠐⡱⢘⠐⠱']
+ - ['\x038a\x03af', '⢘⠐⡊⢘⠐⠊']
+ - ['\x038c\x03cc', '⢘⠐⡕⢘⠐⠕']
+ - ['\x038e\x03cd', '⢘⠐⡥⢘⠐⠥']
+ - ['\x038f\x03ce', '⢘⠐⡺⢘⠐⠺']
+ - ['\x0391\x03b1', '⢘⡁⢘⠁']
+ - ['\x0392\x03b2', '⢘⡃⢘⠃']
+ - ['\x0393\x03b3', '⢘⡛⢘⠛']
+ - ['\x0394\x03b4', '⢘⡙⢘⠙']
+ - ['\x0395\x03b5', '⢘⡑⢘⠑']
+ - ['\x0396\x03b6', '⢘⡵⢘⠵']
+ - ['\x0397\x03b7', '⢘⡱⢘⠱']
+ - ['\x0398\x03b8', '⢘⡹⢘⠹']
+ - ['\x0399\x03b9', '⢘⡊⢘⠊']
+ - ['\x039a\x03ba', '⢘⡅⢘⠅']
+ - ['\x039b\x03bb', '⢘⡇⢘⠇']
+ - ['\x039c\x03bc', '⢘⡍⢘⠍']
+ - ['\x039d\x03bd', '⢘⡝⢘⠝']
+ - ['\x039e\x03be', '⢘⡭⢘⠭']
+ - ['\x039f\x03bf', '⢘⡕⢘⠕']
+ - ['\x03a0\x03c0', '⢘⡏⢘⠏']
+ - ['\x03a1\x03c1', '⢘⡗⢘⠗']
+ - ['\x03a3\x03c3', '⢘⡎⢘⠎']
+ - ['\x03a4\x03c4', '⢘⡞⢘⠞']
+ - ['\x03a5\x03c5', '⢘⡥⢘⠥']
+ - ['\x03a6\x03c6', '⢘⡋⢘⠋']
+ - ['\x03a7\x03c7', '⢘⡯⢘⠯']
+ - ['\x03a8\x03c8', '⢘⡽⢘⠽']
+ - ['\x03a9\x03c9', '⢘⡺⢘⠺']
+
+# Punctuation and bullits
+
+ - ['\x2016', '⠘⢸']
+ - ['\x2017', '⠘⣤']
+
+# Arrows
+
+ - ['\x2190', '⠘⠳⠪']
+ - ['\x2191', '⠘⠳⠬']
+ - ['\x2192', '⠘⠳⠕']
+ - ['\x2193', '⠘⠳⠩']
+ - ['\x2194', '⠘⠳⠺⠗⠕']
+ - ['\x2196', '⠘⠳⠱']
+ - ['\x2197', '⠘⠳⠎']
+ - ['\x2198', '⠘⠳⠣']
+ - ['\x2199', '⠘⠳⠜']
+ - ['\x21D4', '⠘⠳⠺⠶⠗⠕']
+
+# Math signs (experimental)
+
+ - ['\x2200', '⠘⠁']
+ - ['\x2208', '⠘⠑']
+ - ['\x2213', '⠸⠤']
+ - ['\x221d', '⠸⠐⢶']
+ - ['\x2229', '⠨⠦']
+ - ['\x222a', '⠨⠖']
+ - ['\x2243', '⠸⠔']
+ - ['\x2245', '⠐⠘⠔']
+ - ['\x2248', '⠘⠔']
+ - ['\x224f', '⠘⠐⢶']
+ - ['\x2251', '⠨⠐⢶']
+ - ['\x2260', '⠐⢶⠈⠱']
+ - ['\x2261', '⠸⠿']
+ - ['\x2264', '⠸⢔']
+ - ['\x2265', '⠸⡢']
+ - ['\x226a', '⠨⢔']
+ - ['\x226b', '⠨⡢']
+ - ['\x22c5', '⠐⠲']
+
+# Pangram
+
+ - - 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon.'
+ - '⡟⠥⠊⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍⠑⠙ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⡺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏⠡ ⠭⠽⠇⠕⠋⠕⠝⠄'
+
+# Section sign
+
+ - ["§ 3", "⣐⢉"]
+ - ["§ 3:", "⣐⢉⠒"]
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+
+ - ['\x0102\x0103', '⠐⡁⠐⠁']
+ - ['\x0104\x0105', '⠐⡁⠐⠁']
+ - ['\x0108\x0109', '⠐⡉⠐⠉']
+ - ['\x010a\x010b', '⠐⡉⠐⠉']
+ - ['\x010c\x010d', '⠐⡉⠐⠉']
+ - ['\x0110\x0111', '⠐⡙⠐⠙']
+ - ['\x0114\x0115', '⠐⡑⠐⠑']
+ - ['\x0116\x0117', '⠐⡑⠐⠑']
+ - ['\x0118\x0119', '⠐⡑⠐⠑']
+ - ['\x011a\x011b', '⠐⡑⠐⠑']
+ - ['\x011e\x011f', '⠐⡛⠐⠛']
+ - ['\x0120\x0121', '⠐⡛⠐⠛']
+ - ['\x0122\x0123', '⠐⡛⠐⠛']
+ - ['\x0126\x0127', '⠐⡓⠐⠓']
+ - ['\x012a\x012b', '⠐⡊⠐⠊']
+ - ['\x012c\x012d', '⠐⡊⠐⠊']
+ - ['\x012e\x012f', '⠐⡊⠐⠊']
+ - ['\x0130\x0131', '⠐⡊⠐⠊']
+ - ['\x0132\x0133', '⡊⠚⠊⠚']
+ - ['\x013b\x013c', '⠐⡇⠐⠇']
+ - ['\x013d\x013e', '⠐⡇⠐⠇']
+ - ['\x013f\x0140', '⠐⡇⠐⠇']
+ - ['\x0141\x0142', '⠐⡇⠐⠇']
+ - ['\x0145\x0146', '⠐⡝⠐⠝']
+ - ['\x0147\x0148', '⠐⡝⠐⠝']
+ - ['\x0149', '⠈⠝']
+ - ['\x014a\x014b', '⠐⡝⠐⠝']
+ - ['\x014e\x014f', '⠐⡕⠐⠕']
+ - ['\x0150\x0151', '⠐⡕⠐⠕']
+ - ['\x0156\x0157', '⠐⡗⠐⠗']
+ - ['\x0158\x0159', '⠐⡗⠐⠗']
+ - ['\x015c\x015d', '⠐⡎⠐⠎']
+ - ['\x015e\x015f', '⠐⡎⠐⠎']
+ - ['\x0164\x0165', '⠐⡞⠐⠞']
+ - ['\x0166\x0167', '⠐⡞⠐⠞']
+ - ['\x016a\x016b', '⠐⡥⠐⠥']
+ - ['\x016c\x016d', '⠐⡥⠐⠥']
+ - ['\x016e\x016f', '⠐⡥⠐⠥']
+ - ['\x0170\x0171', '⠐⡥⠐⠥']
+ - ['\x0172\x0173', '⠐⡥⠐⠥']
+ - ['\x017b\x017c', '⠐⡵⠐⠵']
+ - ['\x017f', '⠐⠎']
+
+# Punctuation and bullits
+
+ - ['\x2000', ' ']
+ - ['\x2001', ' ']
+ - ['\x2002', ' ']
+ - ['\x2003', ' ']
+ - ['\x2004', ' ']
+ - ['\x2005', ' ']
+ - ['\x2006', ' ']
+ - ['\x2007', ' ']
+ - ['\x2008', ' ']
+ - ['\x2009', ' ']
+ - ['\x200a', ' ']
+ - ['\x2010', '⢤']
+ - ['\x2011', '⢤']
+ - ['\x2012', '⢤']
+ - ['\x201b', '⠈']
+ - ['\x201f', '⠶']
+ - ['\x2023', '⡄']
+ - ['\x202f', ' ']
+ - ['\x203c', '⠖⠖']
+ - ['\x203d', '⠢⠖']
+ - ['\x2043', '⡄']
+ - ['\x2047', '⠢⠢']
+ - ['\x2048', '⠢⠖']
+ - ['\x2049', '⠖⠢']
+ - ['\x204c', '⡄']
+ - ['\x204d', '⡄']
+
+# Geometrical shapes
+
+ - ['\x25e6', '⡄']
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⡑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⡑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⡑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⡑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⡑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⡑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⡑⠝ ⠇⠊⠝⠚⠑ ⠍⠑⠙ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⡑⠞ ⠰⠕⠗⠙⠰ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⡑⠞ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍⠑⠙ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# ---------
+# Grade 1.5
+# ---------
+
+table: {language: da, grade: 1.5, dots: 8, version: 1993, __assert-match: da-dk-g28l_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - [' ', ' ']
+ - ['"', '⠶']
+ - ['$', '⣲']
+ - ['%', '⣚']
+ - ['&', '⢯']
+ - ['''', '⠈']
+ - ['(', '⢦']
+ - [')', '⢴']
+ - ['*', '⠠⠔']
+ - ['+', '⢖']
+ - [',', '⠂']
+ - ['-', '⢤']
+ - ['.', '⠄']
+ - ['/', '⢌']
+ - ['0', '⢚']
+ - ['1', '⢁']
+ - ['2', '⢃']
+ - ['3', '⢉']
+ - ['4', '⢙']
+ - ['5', '⢑']
+ - ['6', '⢋']
+ - ['7', '⢛']
+ - ['8', '⢓']
+ - ['9', '⢊']
+ - [':', '⠒']
+ - [';', '⠆']
+ - ['<', '⢔']
+ - ['=', '⢶']
+ - ['>', '⡢']
+ - ['?', '⠢']
+ - ['@', '⣈']
+ - ['A', '⠠⡁']
+ - ['B', '⠠⡃']
+ - ['C', '⠠⡉']
+ - ['D', '⠠⡙']
+ - ['E', '⠠⡑']
+ - ['F', '⠠⡋']
+ - ['G', '⠠⡛']
+ - ['H', '⠠⡓']
+ - ['I', '⡊']
+ - ['J', '⠠⡚']
+ - ['K', '⠠⡅']
+ - ['L', '⠠⡇']
+ - ['M', '⠠⡍']
+ - ['N', '⠠⡝']
+ - ['O', '⠠⡕']
+ - ['P', '⠠⡏']
+ - ['Q', '⠠⡟']
+ - ['R', '⠠⡗']
+ - ['S', '⠠⡎']
+ - ['T', '⠠⡞']
+ - ['U', '⠠⡥']
+ - ['V', '⠠⡧']
+ - ['W', '⠠⡺']
+ - ['X', '⠠⡭']
+ - ['Y', '⠠⡽']
+ - ['Z', '⠠⡵']
+ - ['[', '⣦']
+ - ['\\', '⠠⡌']
+ - [']', '⣴']
+ - ['^', '⢏']
+ - ['_', '⣤']
+ - ['`', '⠐']
+ - ['a', '⠠⠁']
+ - ['b', '⠠⠃']
+ - ['c', '⠠⠉']
+ - ['d', '⠠⠙']
+ - ['e', '⠠⠑']
+ - ['f', '⠠⠋']
+ - ['g', '⠠⠛']
+ - ['h', '⠠⠓']
+ - ['i', '⠊']
+ - ['j', '⠠⠚']
+ - ['k', '⠠⠅']
+ - ['l', '⠠⠇']
+ - ['m', '⠠⠍']
+ - ['n', '⠠⠝']
+ - ['o', '⠠⠕']
+ - ['p', '⠠⠏']
+ - ['q', '⠠⠟']
+ - ['r', '⠠⠗']
+ - ['s', '⠠⠎']
+ - ['t', '⠠⠞']
+ - ['u', '⠠⠥']
+ - ['v', '⠠⠧']
+ - ['w', '⠠⠺']
+ - ['x', '⠠⠭']
+ - ['y', '⠠⠽']
+ - ['z', '⠠⠵']
+ - ['{', '⣧']
+ - ['|', '⢸']
+ - ['}', '⣼']
+ - ['~', '⡨']
+ - ['€', '⣑']
+ - ['‚', '⡘']
+ - ['ƒ', '⢐']
+ - ['„', '⣆']
+ - ['…', '⠠⠄⠄⠄']
+ - ['†', '⠠⡖']
+ - ['‡', '⠠⣖']
+ - ['ˆ', '⠠⣰']
+ - ['‰', '⣺']
+ - ['Š', '⠠⣎']
+ - ['‹', '⠸']
+ - ['Œ', '⠠⣕']
+ - ['Ž', '⠠⡬']
+ - ['‘', '⡈']
+ - ['’', '⢈']
+ - ['“', '⡆']
+ - ['”', '⢰']
+ - ['•', '⡄']
+ - ['–', '⠠⠤']
+ - ['—', '⠠⡤']
+ - ['˜', '⠨']
+ - ['™', '⣞']
+ - ['š', '⠠⢎']
+ - ['›', '⡸']
+ - ['œ', '⠠⢕']
+ - ['ž', '⠠⠬']
+ - ['Ÿ', '⠠⣾']
+ - [' ', '⢞']
+ - ['¡', '⠠⠲', {xfail: true}]
+ - ['¢', '⣒']
+ - ['£', '⢇']
+ - ['¤', '⠠⡦']
+ - ['¥', '⡠']
+ - ['¦', '⣌']
+ - ['§', '⣐']
+ - ['¨', '⠠⠰']
+ - ['©', '⣭']
+ - ['ª', '⠠⣮']
+ - ['«', '⡐']
+ - ['¬', '⠠⡼']
+ - ['', '⠠⣄']
+ - ['®', '⣗']
+ - ['¯', '⡶']
+ - ['°', '⠠⠴', {xfail: true}]
+ - ['±', '⢟']
+ - ['²', '⢆']
+ - ['³', '⢒']
+ - ['´', '⢨']
+ - ['µ', '⠠⠦']
+ - ['¶', '⢿']
+ - ['·', '⢄']
+ - ['¸', '⣨']
+ - ['¹', '⢂']
+ - ['º', '⠠⣿']
+ - ['»', '⡰']
+ - ['¼', '⢝']
+ - ['½', '⢘']
+ - ['¾', '⠠⠼']
+ - ['À', '⠠⡷']
+ - ['Á', '⠠⣷']
+ - ['Â', '⠠⣡']
+ - ['Ã', '⠠⣩']
+ - ['Ä', '⠠⣜']
+ - ['Å', '⠠⡡']
+ - ['Æ', '⠠⡜']
+ - ['Ç', '⠠⡯']
+ - ['È', '⠠⡮']
+ - ['É', '⠠⡿']
+ - ['Ê', '⠠⡣']
+ - ['Ë', '⠠⡫']
+ - ['Ì', '⠠⣱']
+ - ['Í', '⠠⣣']
+ - ['Î', '⠠⡩']
+ - ['Ï', '⠠⡻']
+ - ['Ð', '⠠⣽']
+ - ['Ñ', '⠠⣻']
+ - ['Ò', '⠠⣫']
+ - ['Ó', '⠠⣬']
+ - ['Ô', '⠠⡹']
+ - ['Õ', '⠠⣹']
+ - ['Ö', '⠠⣪']
+ - ['×', '⢭']
+ - ['Ø', '⠠⡪']
+ - ['Ù', '⠠⡾']
+ - ['Ú', '⠠⣳']
+ - ['Û', '⠠⡱']
+ - ['Ü', '⠠⡳']
+ - ['Ý', '⠠⣍']
+ - ['Þ', '⠠⣅']
+ - ['ß', '⠠⢮']
+ - ['à', '⠠⠷']
+ - ['á', '⠠⢷']
+ - ['â', '⠠⢡']
+ - ['ã', '⠠⢩']
+ - ['ä', '⠠⢜']
+ - ['å', '⠠⠡']
+ - ['æ', '⠠⠜']
+ - ['ç', '⠠⠯']
+ - ['è', '⠠⠮']
+ - ['é', '⠠⠿']
+ - ['ê', '⠠⠣']
+ - ['ë', '⠠⠫']
+ - ['ì', '⠠⢱']
+ - ['í', '⠠⢣']
+ - ['î', '⠠⠩']
+ - ['ï', '⠠⠻']
+ - ['ð', '⠠⢽']
+ - ['ñ', '⠠⢻']
+ - ['ò', '⠠⢫']
+ - ['ó', '⠠⢬']
+ - ['ô', '⠠⠹']
+ - ['õ', '⠠⢹']
+ - ['ö', '⠠⢪']
+ - ['÷', '⢲']
+ - ['ø', '⠠⠪']
+ - ['ù', '⠠⠾']
+ - ['ú', '⠠⢳']
+ - ['û', '⠠⠱']
+ - ['ü', '⠠⠳']
+ - ['ý', '⠠⢍']
+ - ['þ', '⠠⢅']
+ - ['ÿ', '⠠⢾']
+
+# Misc Unicode chars
+# For each accented letter, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ['\x0100\x0101', '⠐⡁⠐⠁']
+ - ['\x0106\x0107', '⠐⡉⠐⠉']
+ - ['\x010e\x010f', '⠐⡙⠐⠙']
+ - ['\x0112\x0113', '⠐⡑⠐⠑']
+ - ['\x011c\x011d', '⠐⡛⠐⠛']
+ - ['\x0124\x0125', '⠐⡓⠐⠓']
+ - ['\x0128\x0129', '⠐⡊⠐⠊']
+ - ['\x0134\x0135', '⠐⡚⠐⠚']
+ - ['\x0136\x0137', '⠐⡅⠐⠅']
+ - ['\x0138', '⠐⠟']
+ - ['\x0139\x013a', '⠐⡇⠐⠇']
+ - ['\x0143\x0144', '⠐⡝⠐⠝']
+ - ['\x014c\x014d', '⠐⡕⠐⠕']
+ - ['\x0154\x0155', '⠐⡗⠐⠗']
+ - ['\x015a\x015b', '⠐⡎⠐⠎']
+ - ['\x0162\x0163', '⠐⡞⠐⠞']
+ - ['\x0168\x0169', '⠐⡥⠐⠥']
+ - ['\x0174\x0175', '⠐⡺⠐⠺']
+ - ['\x0176\x0177', '⠐⡽⠐⠽']
+ - ['\x0179\x017a', '⠐⡵⠐⠵']
+
+# Greek letters (with dots 458 as prefix)
+
+ - ['\x0386\x03ac', '⢘⠐⡁⢘⠐⠁', {xfail: {forward: true}}]
+ - ['\x0388\x03ad', '⢘⠐⡑⢘⠐⠑', {xfail: {forward: true}}]
+ - ['\x0389\x03ae', '⢘⠐⡱⢘⠐⠱', {xfail: {forward: true}}]
+ - ['\x038a\x03af', '⢘⠐⡊⢘⠐⠊', {xfail: {forward: true}}]
+ - ['\x038c\x03cc', '⢘⠐⡕⢘⠐⠕', {xfail: {forward: true}}]
+ - ['\x038e\x03cd', '⢘⠐⡥⢘⠐⠥', {xfail: {forward: true}}]
+ - ['\x038f\x03ce', '⢘⠐⡺⢘⠐⠺', {xfail: {forward: true}}]
+ - ['\x0391\x03b1', '⢘⡁⢘⠁', {xfail: {forward: true}}]
+ - ['\x0392\x03b2', '⢘⡃⢘⠃', {xfail: {forward: true}}]
+ - ['\x0393\x03b3', '⢘⡛⢘⠛', {xfail: {forward: true}}]
+ - ['\x0394\x03b4', '⢘⡙⢘⠙', {xfail: {forward: true}}]
+ - ['\x0395\x03b5', '⢘⡑⢘⠑', {xfail: {forward: true}}]
+ - ['\x0396\x03b6', '⢘⡵⢘⠵', {xfail: {forward: true}}]
+ - ['\x0397\x03b7', '⢘⡱⢘⠱', {xfail: {forward: true}}]
+ - ['\x0398\x03b8', '⢘⡹⢘⠹', {xfail: {forward: true}}]
+ - ['\x0399\x03b9', '⢘⡊⢘⠊', {xfail: {forward: true}}]
+ - ['\x039a\x03ba', '⢘⡅⢘⠅', {xfail: {forward: true}}]
+ - ['\x039b\x03bb', '⢘⡇⢘⠇', {xfail: {forward: true}}]
+ - ['\x039c\x03bc', '⢘⡍⢘⠍', {xfail: {forward: true}}]
+ - ['\x039d\x03bd', '⢘⡝⢘⠝', {xfail: {forward: true}}]
+ - ['\x039e\x03be', '⢘⡭⢘⠭', {xfail: {forward: true}}]
+ - ['\x039f\x03bf', '⢘⡕⢘⠕', {xfail: {forward: true}}]
+ - ['\x03a0\x03c0', '⢘⡏⢘⠏', {xfail: {forward: true}}]
+ - ['\x03a1\x03c1', '⢘⡗⢘⠗', {xfail: {forward: true}}]
+ - ['\x03a3\x03c3', '⢘⡎⢘⠎', {xfail: {forward: true}}]
+ - ['\x03a4\x03c4', '⢘⡞⢘⠞', {xfail: {forward: true}}]
+ - ['\x03a5\x03c5', '⢘⡥⢘⠥', {xfail: {forward: true}}]
+ - ['\x03a6\x03c6', '⢘⡋⢘⠋', {xfail: {forward: true}}]
+ - ['\x03a7\x03c7', '⢘⡯⢘⠯', {xfail: {forward: true}}]
+ - ['\x03a8\x03c8', '⢘⡽⢘⠽', {xfail: {forward: true}}]
+ - ['\x03a9\x03c9', '⢘⡺⢘⠺', {xfail: {forward: true}}]
+
+# Punctuation and bullits
+
+ - ['\x2016', '⠘⢸']
+ - ['\x2017', '⠘⣤']
+
+# Arrows
+
+ - ['\x2190', '⠘⠳⠪']
+ - ['\x2191', '⠘⠳⠬']
+ - ['\x2192', '⠘⠳⠕']
+ - ['\x2193', '⠘⠳⠩']
+ - ['\x2194', '⠘⠳⠺⠗⠕']
+ - ['\x2196', '⠘⠳⠱']
+ - ['\x2197', '⠘⠳⠎']
+ - ['\x2198', '⠘⠳⠣']
+ - ['\x2199', '⠘⠳⠜']
+ - ['\x21D4', '⠘⠳⠺⠶⠗⠕']
+
+# Math signs (experimental)
+
+ - ['\x2200', '⠘⠁']
+ - ['\x2208', '⠘⠑']
+ - ['\x2213', '⠸⠤']
+ - ['\x221d', '⠸⠐⢶']
+ - ['\x2229', '⠨⠦']
+ - ['\x222a', '⠨⠖']
+ - ['\x2243', '⠸⠔']
+ - ['\x2245', '⠐⠘⠔']
+ - ['\x2248', '⠘⠔']
+ - ['\x224f', '⠘⠐⢶']
+ - ['\x2251', '⠨⠐⢶']
+ - ['\x2260', '⠐⢶⠈⠱']
+ - ['\x2261', '⠸⠿']
+ - ['\x2264', '⠸⢔']
+ - ['\x2265', '⠸⡢']
+ - ['\x226a', '⠨⢔']
+ - ['\x226b', '⠨⡢']
+ - ['\x22c5', '⠐⠲']
+
+# Pangram
+
+ - - 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon.'
+ - '⠠⡟⠥⠊⠠⠵⠙⠑⠇⠞⠁⠛⠑⠗⠝⠑ ⠎⠏⠊⠎⠞⠑ ⠚⠕⠗⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠙⠑⠂ ⠍⠑⠝⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠑⠝ ⠠⡺⠁⠇⠞⠓⠑⠗ ⠎⠏⠊⠇⠇⠑⠙⠑ ⠏ ⠠⠭⠽⠇⠕⠋⠕⠝⠄'
+
+# No letsign before numbers
+
+ - ['v8', '⠠⠧⢓']
+ - ['A4', '⠠⡁⢙']
+ - ['Eleva2ren', '⡑⠇⠑⠧⠁⢃⠗⠑⠝']
+
+# URLs emails and file names
+
+ - ['$at', '⣲⠁⠞']
+ - ['\\at\\bliver', '⠠⡌⠁⠞⠠⡌⠃⠇⠊⠧⠑⠗']
+ - ['at@bliver.og', '⠁⠞⣈⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['http://at.bliver.og', '⠓⠞⠞⠏⠒⢌⢌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['www.at.bliver.og', '⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['www.at.bliver.com', '⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍']
+ - ['www.a.b.c', '⠠⠺⠠⠺⠠⠺⠄⠠⠁⠄⠠⠃⠄⠠⠉']
+
+# Section sign
+
+ - ["§ 3", "⣐⢉"]
+ - ["§ 3:", "⣐⢉⠒"]
+
+# Single cell word contractions
+
+ - ['At', '⡁']
+ - ['at', '⠁']
+ - ['Bliver', '⡃']
+ - ['bliver', '⠃']
+ - ['Og', '⡉']
+ - ['og', '⠉']
+ - ['Du', '⡙']
+ - ['du', '⠙']
+ - ['Eller', '⡑']
+ - ['eller', '⠑']
+ - ['For', '⡋']
+ - ['for', '⠋']
+ - ['Gør', '⡛']
+ - ['gør', '⠛']
+ - ['Har', '⡓']
+ - ['har', '⠓']
+ - ['Jeg', '⡚']
+ - ['jeg', '⠚']
+ - ['Kan', '⡅']
+ - ['kan', '⠅']
+ - ['Lige', '⡇']
+ - ['lige', '⠇']
+ - ['Med', '⡍']
+ - ['med', '⠍']
+ - ['Når', '⡝']
+ - ['når', '⠝']
+ - ['Op', '⡕']
+ - ['op', '⠕']
+ - ['På', '⡏']
+ - ['på', '⠏']
+ - ['Under', '⡟']
+ - ['under', '⠟']
+ - ['Rigtig', '⡗']
+ - ['rigtig', '⠗']
+ - ['Som', '⡎']
+ - ['som', '⠎']
+ - ['Til', '⡞']
+ - ['til', '⠞']
+ - ['Hun', '⡥']
+ - ['hun', '⠥']
+ - ['Ved', '⡧']
+ - ['ved', '⠧']
+ - ['Hvad', '⡺']
+ - ['hvad', '⠺']
+ - ['Over', '⡭']
+ - ['over', '⠭']
+ - ['Han', '⡽']
+ - ['han', '⠽']
+ - ['Efter', '⡵']
+ - ['efter', '⠵']
+ - ['Være', '⡜']
+ - ['være', '⠜']
+ - ['Før', '⡪']
+ - ['før', '⠪']
+ - ['Så', '⡡']
+ - ['så', '⠡']
+ - ['Den', '⡯']
+ - ['den', '⠯']
+ - ['Der', '⡾']
+ - ['der', '⠾']
+ - ['Det', '⡮']
+ - ['det', '⠮']
+ - ['De', '⡹']
+ - ['de', '⠹']
+ - ['En', '⡣']
+ - ['en', '⠣']
+ - ['Er', '⡱']
+ - ['er', '⠱']
+ - ['Et', '⡬']
+ - ['et', '⠬']
+ - ['Gennem', '⡻']
+ - ['gennem', '⠻']
+ - ['Hvor', '⡌']
+ - ['hvor', '⠌']
+ - ['Men', '⡩']
+ - ['men', '⠩']
+ - ['Ned', '⡫']
+ - ['ned', '⠫']
+ - ['Ret', '⡷']
+ - ['ret', '⠷']
+ - ['Skal', '⡿']
+ - ['skal', '⠿']
+ - ['Te', '⡳']
+ - ['te', '⠳']
+ - ['Ve', '⡼']
+ - ['ve', '⠼']
+
+# Capsnocont
+
+ - ['UNDER et', '⡥⡝⡙⡑⡗ ⠬']
+
+# No single cell contractions before or after dashes
+
+ - ['at-bliver', '⠁⠞⢤⠃⠇⠊⠧⠑⠗', {xfail: {forward: true}}]
+ - ['d-d-du', '⠠⠙⢤⠠⠙⢤⠙⠥', {xfail: {forward: true}}]
+
+# Combinations with slashes and other punctuation signs
+
+ - ["at!", "⠁⠖"]
+ - ["bliver!", "⠃⠖"]
+ - ["og!", "⠉⠖"]
+ - ['Han/hun', '⡽⢌⠥']
+ - ['han/hun', '⠽⢌⠥']
+ - ['Over/under', '⡭⢌⠟']
+ - ['over/under', '⠭⢌⠟']
+ - ['Til/fra', '⡞⢌⠋', {xfail: true}]
+ - ['til/fra', '⠞⢌⠋', {xfail: true}]
+
+# Combinations which require letsign
+
+ - ['1st', '⢁⠠⠎⠞']
+ - ['2nd', '⢃⠠⠝⠙', {xfail: {forward: true}}]
+ - ['1A', '⢁⠠⡁']
+ - ['1a', '⢁⠠⠁']
+ - ['2B', '⢃⠠⡃']
+ - ['2b', '⢃⠠⠃']
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+ - ['\x0102\x0103', '⠐⡁⠐⠁']
+ - ['\x0104\x0105', '⠐⡁⠐⠁']
+ - ['\x0108\x0109', '⠐⡉⠐⠉']
+ - ['\x010a\x010b', '⠐⡉⠐⠉']
+ - ['\x010c\x010d', '⠐⡉⠐⠉']
+ - ['\x0110\x0111', '⠐⡙⠐⠙']
+ - ['\x0114\x0115', '⠐⡑⠐⠑']
+ - ['\x0116\x0117', '⠐⡑⠐⠑']
+ - ['\x0118\x0119', '⠐⡑⠐⠑']
+ - ['\x011a\x011b', '⠐⡑⠐⠑']
+ - ['\x011e\x011f', '⠐⡛⠐⠛']
+ - ['\x0120\x0121', '⠐⡛⠐⠛']
+ - ['\x0122\x0123', '⠐⡛⠐⠛']
+ - ['\x0126\x0127', '⠐⡓⠐⠓']
+ - ['\x012a\x012b', '⠐⡊⠐⠊']
+ - ['\x012c\x012d', '⠐⡊⠐⠊']
+ - ['\x012e\x012f', '⠐⡊⠐⠊']
+ - ['\x0130\x0131', '⠐⡊⠐⠊']
+ - ['\x0132\x0133', '⡊⠚⠊⠚']
+ - ['\x013b\x013c', '⠐⡇⠐⠇']
+ - ['\x013d\x013e', '⠐⡇⠐⠇']
+ - ['\x013f\x0140', '⠐⡇⠐⠇']
+ - ['\x0141\x0142', '⠐⡇⠐⠇']
+ - ['\x0145\x0146', '⠐⡝⠐⠝']
+ - ['\x0147\x0148', '⠐⡝⠐⠝']
+ - ['\x0149', '⠈⠝']
+ - ['\x014a\x014b', '⠐⡝⠐⠝']
+ - ['\x014e\x014f', '⠐⡕⠐⠕']
+ - ['\x0150\x0151', '⠐⡕⠐⠕']
+ - ['\x0156\x0157', '⠐⡗⠐⠗']
+ - ['\x0158\x0159', '⠐⡗⠐⠗']
+ - ['\x015c\x015d', '⠐⡎⠐⠎']
+ - ['\x015e\x015f', '⠐⡎⠐⠎']
+ - ['\x0164\x0165', '⠐⡞⠐⠞']
+ - ['\x0166\x0167', '⠐⡞⠐⠞']
+ - ['\x016a\x016b', '⠐⡥⠐⠥']
+ - ['\x016c\x016d', '⠐⡥⠐⠥']
+ - ['\x016e\x016f', '⠐⡥⠐⠥']
+ - ['\x0170\x0171', '⠐⡥⠐⠥']
+ - ['\x0172\x0173', '⠐⡥⠐⠥']
+ - ['\x017b\x017c', '⠐⡵⠐⠵']
+ - ['\x017f', '⠐⠎']
+
+# Punctuation and bullits
+
+ - ['\x2000', ' ']
+ - ['\x2001', ' ']
+ - ['\x2002', ' ']
+ - ['\x2003', ' ']
+ - ['\x2004', ' ']
+ - ['\x2005', ' ']
+ - ['\x2006', ' ']
+ - ['\x2007', ' ']
+ - ['\x2008', ' ']
+ - ['\x2009', ' ']
+ - ['\x200a', ' ']
+ - ['\x2010', '⢤']
+ - ['\x2011', '⢤']
+ - ['\x2012', '⢤']
+ - ['\x201b', '⠈']
+ - ['\x201f', '⠶']
+ - ['\x2023', '⡄']
+ - ['\x202f', ' ']
+ - ['\x203c', '⠖⠖']
+ - ['\x203d', '⠢⠖']
+ - ['\x2043', '⡄']
+ - ['\x2047', '⠢⠢']
+ - ['\x2048', '⠢⠖']
+ - ['\x2049', '⠖⠢']
+ - ['\x204c', '⡄']
+ - ['\x204d', '⡄']
+
+# Geometrical shapes
+
+ - ['\x25e6', '⡄']
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⡬ ⠰⠕⠗⠙⠰ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⡬ ⠰⠕⠗⠙⠰ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⡬ ⠰⠕⠗⠙⠰ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠥⠝⠙⠑⠗⠎⠞⠗⠑⠛⠑⠞⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⡬ ⠰⠕⠗⠙⠰ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# -------
+# Grade 2
+# -------
+
+table: {language: da, grade: 2, dots: 8, version: 1993, __assert-match: da-dk-g28_1993.ctb}
+flags: {testmode: bothDirections}
+tests:
+
+# Characters from 0x21 to 0xff
+
+ - [' ', ' ']
+ - ['"', '⠶']
+ - ['$', '⣲']
+ - ['%', '⣚']
+ - ['&', '⢯']
+ - ['''', '⠈']
+ - ['(', '⢦']
+ - [')', '⢴']
+ - ['*', '⠠⠔']
+ - ['+', '⢖']
+ - [',', '⠂']
+ - ['-', '⢤']
+ - ['.', '⠄']
+ - ['/', '⢌']
+ - ['0', '⢚']
+ - ['1', '⢁']
+ - ['2', '⢃']
+ - ['3', '⢉']
+ - ['4', '⢙']
+ - ['5', '⢑']
+ - ['6', '⢋']
+ - ['7', '⢛']
+ - ['8', '⢓']
+ - ['9', '⢊']
+ - [':', '⠒']
+ - [';', '⠆']
+ - ['<', '⢔']
+ - ['=', '⢶']
+ - ['>', '⡢']
+ - ['?', '⠢']
+ - ['@', '⣈']
+ - ['A', '⠠⡁']
+ - ['B', '⠠⡃']
+ - ['C', '⠠⡉']
+ - ['D', '⠠⡙']
+ - ['E', '⠠⡑']
+ - ['F', '⠠⡋']
+ - ['G', '⠠⡛']
+ - ['H', '⠠⡓']
+ - ['I', '⡊']
+ - ['J', '⠠⡚']
+ - ['K', '⠠⡅']
+ - ['L', '⠠⡇']
+ - ['M', '⠠⡍']
+ - ['N', '⠠⡝']
+ - ['O', '⠠⡕']
+ - ['P', '⠠⡏']
+ - ['Q', '⠠⡟']
+ - ['R', '⠠⡗']
+ - ['S', '⠠⡎']
+ - ['T', '⠠⡞']
+ - ['U', '⠠⡥']
+ - ['V', '⠠⡧']
+ - ['W', '⠠⡺']
+ - ['X', '⠠⡭']
+ - ['Y', '⠠⡽']
+ - ['Z', '⠠⡵']
+ - ['[', '⣦']
+ - ['\\', '⠠⡌']
+ - [']', '⣴']
+ - ['^', '⢏']
+ - ['_', '⣤']
+ - ['`', '⠐']
+ - ['a', '⠠⠁']
+ - ['b', '⠠⠃']
+ - ['c', '⠠⠉']
+ - ['d', '⠠⠙']
+ - ['e', '⠠⠑']
+ - ['f', '⠠⠋']
+ - ['g', '⠠⠛']
+ - ['h', '⠠⠓']
+ - ['i', '⠊']
+ - ['j', '⠠⠚']
+ - ['k', '⠠⠅']
+ - ['l', '⠠⠇']
+ - ['m', '⠠⠍']
+ - ['n', '⠠⠝']
+ - ['o', '⠠⠕']
+ - ['p', '⠠⠏']
+ - ['q', '⠠⠟']
+ - ['r', '⠠⠗']
+ - ['s', '⠠⠎']
+ - ['t', '⠠⠞']
+ - ['u', '⠠⠥']
+ - ['v', '⠠⠧']
+ - ['w', '⠠⠺']
+ - ['x', '⠠⠭']
+ - ['y', '⠠⠽']
+ - ['z', '⠠⠵']
+ - ['{', '⣧']
+ - ['|', '⢸']
+ - ['}', '⣼']
+ - ['~', '⡨']
+ - ['€', '⣑']
+ - ['‚', '⡘']
+ - ['ƒ', '⢐']
+ - ['„', '⣆']
+ - ['…', '⠠⠄⠄⠄']
+ - ['†', '⠠⡖']
+ - ['‡', '⠠⣖']
+ - ['ˆ', '⠠⣰']
+ - ['‰', '⣺']
+ - ['Š', '⠠⣎']
+ - ['‹', '⠸']
+ - ['Œ', '⠠⣕']
+ - ['Ž', '⠠⡬']
+ - ['‘', '⡈']
+ - ['’', '⢈']
+ - ['“', '⡆']
+ - ['”', '⢰']
+ - ['•', '⡄']
+ - ['–', '⠠⠤']
+ - ['—', '⠠⡤']
+ - ['˜', '⠨']
+ - ['™', '⣞']
+ - ['š', '⠠⢎']
+ - ['›', '⡸']
+ - ['œ', '⠠⢕']
+ - ['ž', '⠠⠬']
+ - ['Ÿ', '⠠⣾']
+ - [' ', '⢞']
+ - ['¡', '⠠⠲']
+ - ['¢', '⣒']
+ - ['£', '⢇']
+ - ['¤', '⠠⡦']
+ - ['¥', '⡠']
+ - ['¦', '⣌']
+ - ['§', '⣐']
+ - ['¨', '⠠⠰']
+ - ['©', '⣭']
+ - ['ª', '⠠⣮']
+ - ['«', '⡐']
+ - ['¬', '⠠⡼']
+ - ['', '⠠⣄']
+ - ['®', '⣗']
+ - ['¯', '⡶']
+ - ['°', '⠈⠴']
+ - ['±', '⢟']
+ - ['²', '⢆']
+ - ['³', '⢒']
+ - ['´', '⢨']
+ - ['µ', '⠠⠦']
+ - ['¶', '⢿']
+ - ['·', '⢄']
+ - ['¸', '⣨']
+ - ['¹', '⢂']
+ - ['º', '⠠⣿']
+ - ['»', '⡰']
+ - ['¼', '⢝']
+ - ['½', '⢘']
+ - ['¾', '⠠⠼']
+ - ['À', '⠠⡷']
+ - ['Á', '⠠⣷']
+ - ['Â', '⠠⣡']
+ - ['Ã', '⠠⣩']
+ - ['Ä', '⠠⣜']
+ - ['Å', '⠠⡡']
+ - ['Æ', '⠠⡜']
+ - ['Ç', '⠠⡯']
+ - ['È', '⠠⡮']
+ - ['É', '⠠⡿']
+ - ['Ê', '⠠⡣']
+ - ['Ë', '⠠⡫']
+ - ['Ì', '⠠⣱']
+ - ['Í', '⠠⣣']
+ - ['Î', '⠠⡩']
+ - ['Ï', '⠠⡻']
+ - ['Ð', '⠠⣽']
+ - ['Ñ', '⠠⣻']
+ - ['Ò', '⠠⣫']
+ - ['Ó', '⠠⣬']
+ - ['Ô', '⠠⡹']
+ - ['Õ', '⠠⣹']
+ - ['Ö', '⠠⣪']
+ - ['×', '⢭']
+ - ['Ø', '⠠⡪']
+ - ['Ù', '⠠⡾']
+ - ['Ú', '⠠⣳']
+ - ['Û', '⠠⡱']
+ - ['Ü', '⠠⡳']
+ - ['Ý', '⠠⣍']
+ - ['Þ', '⠠⣅']
+ - ['ß', '⠠⢮']
+ - ['à', '⠠⠷']
+ - ['á', '⠠⢷']
+ - ['â', '⠠⢡']
+ - ['ã', '⠠⢩']
+ - ['ä', '⠠⢜']
+ - ['å', '⠠⠡']
+ - ['æ', '⠠⠜']
+ - ['ç', '⠠⠯']
+ - ['è', '⠠⠮']
+ - ['é', '⠠⠿']
+ - ['ê', '⠠⠣']
+ - ['ë', '⠠⠫']
+ - ['ì', '⠠⢱']
+ - ['í', '⠠⢣']
+ - ['î', '⠠⠩']
+ - ['ï', '⠠⠻']
+ - ['ð', '⠠⢽']
+ - ['ñ', '⠠⢻']
+ - ['ò', '⠠⢫']
+ - ['ó', '⠠⢬']
+ - ['ô', '⠠⠹']
+ - ['õ', '⠠⢹']
+ - ['ö', '⠠⢪']
+ - ['÷', '⢲']
+ - ['ø', '⠠⠪']
+ - ['ù', '⠠⠾']
+ - ['ú', '⠠⢳']
+ - ['û', '⠠⠱']
+ - ['ü', '⠠⠳']
+ - ['ý', '⠠⢍']
+ - ['þ', '⠠⢅']
+ - ['ÿ', '⠠⢾']
+
+# Misc Unicode chars
+# For each accented letter, the first occurring instance is chosen for back-translation.
+# There are no rules about this in the specs of Danish Braille,
+# So the choice is arbitrary and may change over time.
+
+ - ['\x0100\x0101', '⠐⡁⠐⠁']
+ - ['\x0106\x0107', '⠐⡉⠐⠉']
+ - ['\x010e\x010f', '⠐⡙⠐⠙']
+ - ['\x0112\x0113', '⠐⡑⠐⠑']
+ - ['\x011c\x011d', '⠐⡛⠐⠛']
+ - ['\x0124\x0125', '⠐⡓⠐⠓']
+ - ['\x0128\x0129', '⠐⡊⠐⠊']
+ - ['\x0134\x0135', '⠐⡚⠐⠚']
+ - ['\x0136\x0137', '⠐⡅⠐⠅']
+ - ['\x0138', '⠐⠟']
+ - ['\x0139\x013a', '⠐⡇⠐⠇']
+ - ['\x0143\x0144', '⠐⡝⠐⠝']
+ - ['\x014c\x014d', '⠐⡕⠐⠕']
+ - ['\x0154\x0155', '⠐⡗⠐⠗']
+ - ['\x015a\x015b', '⠐⡎⠐⠎']
+ - ['\x0162\x0163', '⠐⡞⠐⠞']
+ - ['\x0168\x0169', '⠐⡥⠐⠥']
+ - ['\x0174\x0175', '⠐⡺⠐⠺']
+ - ['\x0176\x0177', '⠐⡽⠐⠽']
+ - ['\x0179\x017a', '⠐⡵⠐⠵']
+
+# Greek letters (with dots 458 as prefix)
+
+ - ['\x0386\x03ac', '⢘⠐⡁⢘⠐⠁', {xfail: {forward: true}}]
+ - ['\x0388\x03ad', '⢘⠐⡑⢘⠐⠑', {xfail: {forward: true}}]
+ - ['\x0389\x03ae', '⢘⠐⡱⢘⠐⠱', {xfail: {forward: true}}]
+ - ['\x038a\x03af', '⢘⠐⡊⢘⠐⠊', {xfail: {forward: true}}]
+ - ['\x038c\x03cc', '⢘⠐⡕⢘⠐⠕', {xfail: {forward: true}}]
+ - ['\x038e\x03cd', '⢘⠐⡥⢘⠐⠥', {xfail: {forward: true}}]
+ - ['\x038f\x03ce', '⢘⠐⡺⢘⠐⠺', {xfail: {forward: true}}]
+ - ['\x0391\x03b1', '⢘⡁⢘⠁', {xfail: {forward: true}}]
+ - ['\x0392\x03b2', '⢘⡃⢘⠃', {xfail: {forward: true}}]
+ - ['\x0393\x03b3', '⢘⡛⢘⠛', {xfail: {forward: true}}]
+ - ['\x0394\x03b4', '⢘⡙⢘⠙', {xfail: {forward: true}}]
+ - ['\x0395\x03b5', '⢘⡑⢘⠑', {xfail: {forward: true}}]
+ - ['\x0396\x03b6', '⢘⡵⢘⠵', {xfail: {forward: true}}]
+ - ['\x0397\x03b7', '⢘⡱⢘⠱', {xfail: {forward: true}}]
+ - ['\x0398\x03b8', '⢘⡹⢘⠹', {xfail: {forward: true}}]
+ - ['\x0399\x03b9', '⢘⡊⢘⠊', {xfail: {forward: true}}]
+ - ['\x039a\x03ba', '⢘⡅⢘⠅', {xfail: {forward: true}}]
+ - ['\x039b\x03bb', '⢘⡇⢘⠇', {xfail: {forward: true}}]
+ - ['\x039c\x03bc', '⢘⡍⢘⠍', {xfail: {forward: true}}]
+ - ['\x039d\x03bd', '⢘⡝⢘⠝', {xfail: {forward: true}}]
+ - ['\x039e\x03be', '⢘⡭⢘⠭', {xfail: {forward: true}}]
+ - ['\x039f\x03bf', '⢘⡕⢘⠕', {xfail: {forward: true}}]
+ - ['\x03a0\x03c0', '⢘⡏⢘⠏', {xfail: {forward: true}}]
+ - ['\x03a1\x03c1', '⢘⡗⢘⠗', {xfail: {forward: true}}]
+ - ['\x03a3\x03c3', '⢘⡎⢘⠎', {xfail: {forward: true}}]
+ - ['\x03a4\x03c4', '⢘⡞⢘⠞', {xfail: {forward: true}}]
+ - ['\x03a5\x03c5', '⢘⡥⢘⠥', {xfail: {forward: true}}]
+ - ['\x03a6\x03c6', '⢘⡋⢘⠋', {xfail: {forward: true}}]
+ - ['\x03a7\x03c7', '⢘⡯⢘⠯', {xfail: {forward: true}}]
+ - ['\x03a8\x03c8', '⢘⡽⢘⠽', {xfail: {forward: true}}]
+ - ['\x03a9\x03c9', '⢘⡺⢘⠺', {xfail: {forward: true}}]
+
+# Punctuation and bullits
+
+ - ['\x2016', '⠘⢸']
+ - ['\x2017', '⠘⣤']
+
+# Arrows
+
+ - ['\x2190', '⠘⠳⠪']
+ - ['\x2191', '⠘⠳⠬']
+ - ['\x2192', '⠘⠳⠕']
+ - ['\x2193', '⠘⠳⠩']
+ - ['\x2194', '⠘⠳⠺⠗⠕']
+ - ['\x2196', '⠘⠳⠱']
+ - ['\x2197', '⠘⠳⠎']
+ - ['\x2198', '⠘⠳⠣']
+ - ['\x2199', '⠘⠳⠜']
+ - ['\x21D4', '⠘⠳⠺⠶⠗⠕']
+
+# Math signs (experimental)
+
+ - ['\x2200', '⠘⠁']
+ - ['\x2208', '⠘⠑']
+ - ['\x2213', '⠸⠤']
+ - ['\x221d', '⠸⠐⢶']
+ - ['\x2229', '⠨⠦']
+ - ['\x222a', '⠨⠖']
+ - ['\x2243', '⠸⠔']
+ - ['\x2245', '⠐⠘⠔']
+ - ['\x2248', '⠘⠔']
+ - ['\x224f', '⠘⠐⢶']
+ - ['\x2251', '⠨⠐⢶']
+ - ['\x2260', '⠐⢶⠈⠱']
+ - ['\x2261', '⠸⠿']
+ - ['\x2264', '⠸⢔']
+ - ['\x2265', '⠸⡢']
+ - ['\x226a', '⠨⢔']
+ - ['\x226b', '⠨⡢']
+ - ['\x22c5', '⠐⠲']
+
+# Pangram
+
+ - - 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon.'
+ - '⠠⡟⠥⠊⠠⠵⠹⠇⠞⠁⠛⠱⠫ ⠎⠏⠊⠵⠑ ⠚⠭⠙⠃⠜⠗ ⠍ ⠋⠇⠪⠹⠂ ⠍⠣⠎ ⠉⠊⠗⠅⠥⠎⠅⠇⠕⠧⠝⠣ ⠠⡺⠁⠇⠞⠓⠱ ⠎⠏⠊⠇⠇⠑⠹ ⠏ ⠠⠭⠽⠇⠕⠋⠕⠝⠄'
+
+# Inverted exclamation
+
+ - ['¡Que lastima!', '⠠⠲⠠⡟⠥⠑ ⠇⠁⠵⠊⠍⠁⠖']
+
+# No letsign before numbers
+
+ - ['v8', '⠠⠧⢓']
+ - ['A4', '⠠⡁⢙']
+ - ['Eleva2ren', '⡑⠇⠑⠧⠁⢃⠗⠣']
+
+# URLs emails and file names
+
+ - ['$at', '⣲⠁⠞']
+ - ['\\at\\bliver', '⠠⡌⠁⠞⠠⡌⠃⠇⠊⠧⠑⠗']
+ - ['at@bliver.og', '⠁⠞⣈⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['http://at.bliver.og', '⠓⠞⠞⠏⠒⢌⢌⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['www.at.bliver.og', '⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠕⠛']
+ - ['www.at.bliver.com', '⠠⠺⠠⠺⠠⠺⠄⠁⠞⠄⠃⠇⠊⠧⠑⠗⠄⠉⠕⠍']
+ - ['www.a.b.c', '⠠⠺⠠⠺⠠⠺⠄⠠⠁⠄⠠⠃⠄⠠⠉']
+ - ['test.txt', '⠳⠵⠄⠞⠠⠭⠞']
+
+# Section sign
+
+ - ["§ 3", "⣐⢉"]
+ - ["§ 3:", "⣐⢉⠒"]
+
+# Word contractions
+
+ - ['At', '⡁']
+ - ['at', '⠁']
+ - ['Aldrig', '⡁⠔']
+ - ['aldrig', '⠁⠔']
+ - ['aig', '⠁⠊⠛']
+ - ['Alle', '⡁⠑']
+ - ['alle', '⠁⠑']
+ - ['ae', '⠠⠁⠑']
+ - ['Allerede', '⡁⠇⠗']
+ - ['allerede', '⠁⠇⠗']
+ - ['alr', '⠠⠁⠇⠗']
+ - ['Alligevel', '⡁⠇⠧']
+ - ['alligevel', '⠁⠇⠧']
+ - ['alv', '⠠⠁⠇⠧']
+ - ['Altid', '⡁⠞⠙']
+ - ['altid', '⠁⠞⠙']
+ - ['atd', '⠠⠁⠞⠙']
+ - ['Altså', '⡁⠡']
+ - ['altså', '⠁⠡']
+ - ['aå', '⠠⠁⠡']
+ - ['Bliver', '⡃']
+ - ['bliver', '⠃']
+ - ['Og', '⡉']
+ - ['og', '⠉']
+ - ['Deres', '⡲']
+ - ['deres', '⠲']
+ - ['Du', '⡙']
+ - ['du', '⠙']
+ - ['Eller', '⡑']
+ - ['eller', '⠑']
+ - ['For', '⡋']
+ - ['for', '⠋']
+ - ['Gør', '⡛']
+ - ['gør', '⠛']
+ - ['Har', '⡓']
+ - ['har', '⠓']
+ - ['Jeg', '⡚']
+ - ['jeg', '⠚']
+ - ['Kan', '⡅']
+ - ['kan', '⠅']
+ - ['Lige', '⡇']
+ - ['lige', '⠇']
+ - ['Med', '⡍']
+ - ['med', '⠍']
+ - ['Når', '⡝']
+ - ['når', '⠝']
+ - ['Op', '⡕']
+ - ['op', '⠕']
+ - ['På', '⡏']
+ - ['på', '⠏']
+ - ['Under', '⡟']
+ - ['under', '⠟']
+ - ['Rigtig', '⡗']
+ - ['rigtig', '⠗']
+ - ['Som', '⡎']
+ - ['som', '⠎']
+ - ['Til', '⡞']
+ - ['til', '⠞']
+ - ['Hun', '⡥']
+ - ['hun', '⠥']
+ - ['Ved', '⡧']
+ - ['ved', '⠧']
+ - ['Hvad', '⡺']
+ - ['hvad', '⠺']
+ - ['Over', '⡭']
+ - ['over', '⠭']
+ - ['Han', '⡽']
+ - ['han', '⠽']
+ - ['Efter', '⡵']
+ - ['efter', '⠵']
+ - ['Være', '⡜']
+ - ['være', '⠜']
+ - ['Før', '⡪']
+ - ['før', '⠪']
+ - ['Så', '⡡']
+ - ['så', '⠡']
+ - ['Den', '⡯']
+ - ['den', '⠯']
+ - ['Der', '⡾']
+ - ['der', '⠾']
+ - ['Det', '⡮']
+ - ['det', '⠮']
+ - ['De', '⡹']
+ - ['de', '⠹']
+ - ['En', '⡣']
+ - ['en', '⠣']
+ - ['Er', '⡱']
+ - ['er', '⠱']
+ - ['Et', '⡬']
+ - ['et', '⠬']
+ - ['Gennem', '⡻']
+ - ['gennem', '⠻']
+ - ['Hvor', '⡌']
+ - ['hvor', '⠌']
+ - ['Men', '⡩']
+ - ['men', '⠩']
+ - ['Ned', '⡫']
+ - ['ned', '⠫']
+ - ['Ret', '⡷']
+ - ['ret', '⠷']
+ - ['Skal', '⡿']
+ - ['skal', '⠿']
+ - ['Te', '⡳']
+ - ['te', '⠳']
+ - ['Ve', '⡼']
+ - ['ve', '⠼']
+
+# Partword/nocross
+
+ - ['Denne', '⡯⠫']
+ - ['denne', '⠯⠫']
+ - ['Mændene', '⡍⠜⠝⠹⠫']
+ - ['mændene', '⠍⠜⠝⠹⠫']
+ - ['Derhos', '⡾⠓⠕⠎']
+ - ['derhos', '⠾⠓⠕⠎']
+ - ['Hunderace', '⡓⠥⠝⠹⠗⠁⠉⠑']
+ - ['hunderace', '⠓⠥⠝⠹⠗⠁⠉⠑']
+ - ['Dette', '⡮⠳']
+ - ['dette', '⠮⠳']
+ - ['Detalje', '⡹⠞⠁⠇⠚⠑']
+ - ['detalje', '⠹⠞⠁⠇⠚⠑']
+
+# Nocross multiple cells
+
+ - ['Endda', '⡑⠟⠙⠁']
+ - ['endda', '⠑⠟⠙⠁']
+ - ['Morgendag', '⡍⠭⠛⠣⠙⠁⠛']
+ - ['morgendag', '⠍⠭⠛⠣⠙⠁⠛']
+ - ['Gendanne', '⡛⠣⠙⠁⠝⠫']
+ - ['gendanne', '⠛⠣⠙⠁⠝⠫']
+ - ['Generelt', '⡻⠫⠷⠇⠞']
+ - ['generelt', '⠻⠫⠷⠇⠞']
+
+ - ['Fra!', '⡋⠗⠁⠖']
+ - ['fra!', '⠋⠗⠁⠖']
+ - ['!Fra', '⠠⠖⡖']
+ - ['!fra', '⠖⠋⠗⠁']
+ - ['''Af', '⠈⡴']
+ - ['''af', '⠈⠁⠋']
+
+# Capsnocont
+
+ - ['UNDER et', '⡥⡝⡙⡑⡗ ⠬']
+
+# No single cell contractions before or after dashes
+
+ - ['at-bliver', '⠁⠞⢤⠃']
+ - ['d-d-du', '⠠⠙⢤⠠⠙⢤⠙⠥']
+
+# Combinations with slashes and other punctuation signs
+
+ - ["at!", "⠁⠖"]
+ - ["bliver!", "⠃⠖"]
+ - ["og!", "⠉⠖"]
+ - ['Han/hun', '⡽⢌⠥']
+ - ['han/hun', '⠽⢌⠥']
+ - ['Over/under', '⡭⢌⠟']
+ - ['over/under', '⠭⢌⠟']
+ - ['Til/fra', '⡞⢌⠖']
+ - ['til/fra', '⠞⢌⠖']
+
+# Combinations which require letsign
+
+ - ['1st', '⢁⠠⠎⠞']
+ - ['2nd', '⢃⠝⠙']
+ - ['1A', '⢁⠠⡁']
+ - ['1a', '⢁⠠⠁']
+ - ['2B', '⢃⠠⡃']
+ - ['2b', '⢃⠠⠃']
+
+# Characters and constructs which cannot be properly back-translated
+
+flags: {testmode: forward}
+tests:
+ - ['\x0102\x0103', '⠐⡁⠐⠁']
+ - ['\x0104\x0105', '⠐⡁⠐⠁']
+ - ['\x0108\x0109', '⠐⡉⠐⠉']
+ - ['\x010a\x010b', '⠐⡉⠐⠉']
+ - ['\x010c\x010d', '⠐⡉⠐⠉']
+ - ['\x0110\x0111', '⠐⡙⠐⠙']
+ - ['\x0114\x0115', '⠐⡑⠐⠑']
+ - ['\x0116\x0117', '⠐⡑⠐⠑']
+ - ['\x0118\x0119', '⠐⡑⠐⠑']
+ - ['\x011a\x011b', '⠐⡑⠐⠑']
+ - ['\x011e\x011f', '⠐⡛⠐⠛']
+ - ['\x0120\x0121', '⠐⡛⠐⠛']
+ - ['\x0122\x0123', '⠐⡛⠐⠛']
+ - ['\x0126\x0127', '⠐⡓⠐⠓']
+ - ['\x012a\x012b', '⠐⡊⠐⠊']
+ - ['\x012c\x012d', '⠐⡊⠐⠊']
+ - ['\x012e\x012f', '⠐⡊⠐⠊']
+ - ['\x0130\x0131', '⠐⡊⠐⠊']
+ - ['\x0132\x0133', '⡊⠚⠊⠚']
+ - ['\x013b\x013c', '⠐⡇⠐⠇']
+ - ['\x013d\x013e', '⠐⡇⠐⠇']
+ - ['\x013f\x0140', '⠐⡇⠐⠇']
+ - ['\x0141\x0142', '⠐⡇⠐⠇']
+ - ['\x0145\x0146', '⠐⡝⠐⠝']
+ - ['\x0147\x0148', '⠐⡝⠐⠝']
+ - ['\x0149', '⠈⠝']
+ - ['\x014a\x014b', '⠐⡝⠐⠝']
+ - ['\x014e\x014f', '⠐⡕⠐⠕']
+ - ['\x0150\x0151', '⠐⡕⠐⠕']
+ - ['\x0156\x0157', '⠐⡗⠐⠗']
+ - ['\x0158\x0159', '⠐⡗⠐⠗']
+ - ['\x015c\x015d', '⠐⡎⠐⠎']
+ - ['\x015e\x015f', '⠐⡎⠐⠎']
+ - ['\x0164\x0165', '⠐⡞⠐⠞']
+ - ['\x0166\x0167', '⠐⡞⠐⠞']
+ - ['\x016a\x016b', '⠐⡥⠐⠥']
+ - ['\x016c\x016d', '⠐⡥⠐⠥']
+ - ['\x016e\x016f', '⠐⡥⠐⠥']
+ - ['\x0170\x0171', '⠐⡥⠐⠥']
+ - ['\x0172\x0173', '⠐⡥⠐⠥']
+ - ['\x017b\x017c', '⠐⡵⠐⠵']
+ - ['\x017f', '⠐⠎']
+
+# Punctuation and bullits
+
+ - ['\x2000', ' ']
+ - ['\x2001', ' ']
+ - ['\x2002', ' ']
+ - ['\x2003', ' ']
+ - ['\x2004', ' ']
+ - ['\x2005', ' ']
+ - ['\x2006', ' ']
+ - ['\x2007', ' ']
+ - ['\x2008', ' ']
+ - ['\x2009', ' ']
+ - ['\x200a', ' ']
+ - ['\x2010', '⢤']
+ - ['\x2011', '⢤']
+ - ['\x2012', '⢤']
+ - ['\x201b', '⠈']
+ - ['\x201f', '⠶']
+ - ['\x2023', '⡄']
+ - ['\x202f', ' ']
+ - ['\x203c', '⠖⠖']
+ - ['\x203d', '⠢⠖']
+ - ['\x2043', '⡄']
+ - ['\x2047', '⠢⠢']
+ - ['\x2048', '⠢⠖']
+ - ['\x2049', '⠖⠢']
+ - ['\x204c', '⡄']
+ - ['\x204d', '⡄']
+
+# Geometrical shapes
+
+ - ['\x25e6', '⡄']
+
+# Emphasis
+
+ - # Bold line
+ - En linje med fed.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠋⠑⠙⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ - # Bold word
+ - Et ord med fed.
+ - ⡬ ⠰⠭⠙⠰ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' +++ '
+ - # Bold letter
+ - Et bogstav med fed.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠋⠑⠙⠄
+ - typeform:
+ bold: ' + '
+
+ - # Italic line
+ - En linje med kursiv.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠅⠥⠗⠎⠊⠧⠄⠰
+ - typeform:
+ italic: '++++++++++++++++++++'
+ - # Italic word
+ - Et ord med kursiv.
+ - ⡬ ⠰⠭⠙⠰ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' +++ '
+ - # Italic letter
+ - Et bogstav med kursiv.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠅⠥⠗⠎⠊⠧⠄
+ - typeform:
+ italic: ' + '
+
+ - # Underlined line
+ - En linje med understreget.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄⠰
+ - typeform:
+ underline: '++++++++++++++++++++++++++'
+ - # Underlined word
+ - Et ord med understreget.
+ - ⡬ ⠰⠭⠙⠰ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' +++ '
+ - # Underlined letter
+ - Et bogstav med understreget.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠥⠝⠾⠵⠷⠛⠬⠄
+ - typeform:
+ underline: ' + '
+
+ - # Bold, italic and underlined line
+ - En linje med alt.
+ - ⠰⡣ ⠇⠊⠝⠚⠑ ⠍ ⠁⠇⠞⠄⠰
+ - typeform:
+ bold: '+++++++++++++++++'
+ italic: '+++++++++++++++++'
+ underline: '+++++++++++++++++'
+ - # Bold, italic and underlined word
+ - Et ord med alt.
+ - ⡬ ⠰⠭⠙⠰ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' +++ '
+ italic: ' +++ '
+ underline: ' +++ '
+ - # Bold, italic and underlined letter
+ - Et bogstav med alt.
+ - ⡬ ⠃⠕⠛⠰⠎⠰⠞⠁⠧ ⠍ ⠁⠇⠞⠄
+ - typeform:
+ bold: ' + '
+ italic: ' + '
+ underline: ' + '
+
+# Space characters
+
+display: |
+ include unicode-without-blank.dis
+ display a a
+
+table: {language: da, grade: 0, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 1, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 1.5, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 2, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 1, dots: 6, direction: forward, version: 1993}
+table: {language: da, grade: 1.5, dots: 6, direction: forward, version: 1993}
+table: {language: da, grade: 2, dots: 6, direction: forward, version: 1993}
+table: {language: da, grade: 1, dots: 6, direction: both, version: 1993}
+table: {language: da, grade: 1.5, dots: 6, direction: both, version: 1993}
+table: {language: da, grade: 2, dots: 6, direction: both, version: 1993}
+flags: {testmode: bothDirections}
+tests:
+ - ['\x0009', '⣊']
+ - ['\x000a', '⣠']
+ - ['\x000b', '⢥']
+ - ['\x000c', '⣇']
+ - ['\x000d', '⡒']
+ - ['\x0020', '\x0020']
+
+table: {language: da, grade: 0, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 1, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 1.5, dots: 8, direction: both, version: 1993}
+table: {language: da, grade: 2, dots: 8, direction: both, version: 1993}
+flags: {testmode: bothDirections}
+tests:
+ - ['\x00a0', '⢞']
+
+table: {language: da, grade: 1, dots: 6, direction: forward, version: 1993}
+table: {language: da, grade: 1.5, dots: 6, direction: forward, version: 1993}
+table: {language: da, grade: 2, dots: 6, direction: forward, version: 1993}
+table: {language: da, grade: 1, dots: 6, direction: both, version: 1993}
+table: {language: da, grade: 1.5, dots: 6, direction: both, version: 1993}
+table: {language: da, grade: 2, dots: 6, direction: both, version: 1993}
+flags: {testmode: bothDirections}
+tests:
+ - ['\x00a0', 'a']
diff --git a/LibLouis.NET.Test/nota-tables/README.md b/LibLouis.NET.Test/nota-tables/README.md
new file mode 100644
index 0000000..da6339e
--- /dev/null
+++ b/LibLouis.NET.Test/nota-tables/README.md
@@ -0,0 +1,31 @@
+# Nota's Danish tables, for the tests
+
+Kept in their own directory, and copied to `nota-tables/` in the output rather than `tables/`.
+
+`LibLouis.NET.Tables` copies the upstream table set into `tables/`. Five of the files here share a
+name with an upstream table and differ from it, so a single shared directory would make which copy
+a test gets depend on MSBuild item ordering. Separate directories mean every test states which set
+it means, and the upstream braille specs can be checked against upstream tables without this set
+shadowing them.
+
+## What these are
+
+A fork of an older upstream, not a patch on the current one. They add things upstream does not
+have, such as the `foreign` emphasis class behind `TypeForm.ForeignLanguage`, and they are missing
+things upstream has since fixed, such as the rules that remove the space between `§` and a following
+number. Do not assume a file here matches the upstream file of the same name in either direction.
+
+Two of them, `da-dk-g16-markers.ctb` and `da-dk-braillo.dis`, exist nowhere upstream, which is why
+the tests cannot simply use the upstream set.
+
+## These are not canonical
+
+The canonical Nota tables ship with the application. This is a copy, and a stale one: only seven of
+the thirty files are reachable from any test.
+
+Reachable: `da-dk-g16-markers.ctb`, `da-dk-braillo.dis`, `da-dk-g08.ctb`, `da-dk-g26.ctb`, and the
+three they include (`da-dk-6miscChars.cti`, `da-dk-octobraille.dis`, `da-dk-g2.dic`).
+
+The other twenty three are referenced by nothing. They are kept for now, but nothing keeps them in
+step with the application's copy, so treat any of them as evidence of nothing. Prefer adding a test
+that needs a file over adding a file that no test needs.
diff --git a/LibLouis.NET.Test/tables/da-dk-6miscChars.cti b/LibLouis.NET.Test/nota-tables/da-dk-6miscChars.cti
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-6miscChars.cti
rename to LibLouis.NET.Test/nota-tables/da-dk-6miscChars.cti
diff --git a/LibLouis.NET.Test/tables/da-dk-6miscChars_1993.cti b/LibLouis.NET.Test/nota-tables/da-dk-6miscChars_1993.cti
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-6miscChars_1993.cti
rename to LibLouis.NET.Test/nota-tables/da-dk-6miscChars_1993.cti
diff --git a/LibLouis.NET.Test/tables/da-dk-8miscChars.cti b/LibLouis.NET.Test/nota-tables/da-dk-8miscChars.cti
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-8miscChars.cti
rename to LibLouis.NET.Test/nota-tables/da-dk-8miscChars.cti
diff --git a/LibLouis.NET.Test/tables/da-dk-8miscChars_1993.cti b/LibLouis.NET.Test/nota-tables/da-dk-8miscChars_1993.cti
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-8miscChars_1993.cti
rename to LibLouis.NET.Test/nota-tables/da-dk-8miscChars_1993.cti
diff --git a/LibLouis.NET.Test/tables/da-dk-braillo.dis b/LibLouis.NET.Test/nota-tables/da-dk-braillo.dis
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-braillo.dis
rename to LibLouis.NET.Test/nota-tables/da-dk-braillo.dis
diff --git a/LibLouis.NET.Test/tables/da-dk-g08.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g08.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g08.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g08.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g08_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g08_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g08_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g08_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g16-crossword.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g16-crossword.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g16-crossword.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g16-crossword.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g16-lit_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g16-lit_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g16-lit_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g16-lit_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g16-markers.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g16-markers.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g16-markers.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g16-markers.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g16.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g16.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g16.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g16.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g16_1993-markers.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g16_1993-markers.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g16_1993-markers.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g16_1993-markers.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g16_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g16_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g16_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g16_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g18.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g18.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g18.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g18.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g18_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g18_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g18_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g18_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g2.dic b/LibLouis.NET.Test/nota-tables/da-dk-g2.dic
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g2.dic
rename to LibLouis.NET.Test/nota-tables/da-dk-g2.dic
diff --git a/LibLouis.NET.Test/tables/da-dk-g26-lit_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26-lit_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26-lit_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26-lit_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g26.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g26_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g26l-lit.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26l-lit.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26l-lit.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26l-lit.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g26l-lit_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26l-lit_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26l-lit_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26l-lit_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g26l.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26l.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26l.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26l.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g26l_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g26l_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g26l_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g26l_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g28.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g28.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g28.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g28.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g28_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g28_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g28_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g28_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g28l.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g28l.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g28l.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g28l.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g28l_1993.ctb b/LibLouis.NET.Test/nota-tables/da-dk-g28l_1993.ctb
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g28l_1993.ctb
rename to LibLouis.NET.Test/nota-tables/da-dk-g28l_1993.ctb
diff --git a/LibLouis.NET.Test/tables/da-dk-g2_1993.dic b/LibLouis.NET.Test/nota-tables/da-dk-g2_1993.dic
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-g2_1993.dic
rename to LibLouis.NET.Test/nota-tables/da-dk-g2_1993.dic
diff --git a/LibLouis.NET.Test/tables/da-dk-octobraille.dis b/LibLouis.NET.Test/nota-tables/da-dk-octobraille.dis
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-octobraille.dis
rename to LibLouis.NET.Test/nota-tables/da-dk-octobraille.dis
diff --git a/LibLouis.NET.Test/tables/da-dk-octobraille_1993.dis b/LibLouis.NET.Test/nota-tables/da-dk-octobraille_1993.dis
similarity index 100%
rename from LibLouis.NET.Test/tables/da-dk-octobraille_1993.dis
rename to LibLouis.NET.Test/nota-tables/da-dk-octobraille_1993.dis
diff --git a/LibLouis.NET/LibLouis.cs b/LibLouis.NET/LibLouis.cs
index 22bb72b..6d8e661 100644
--- a/LibLouis.NET/LibLouis.cs
+++ b/LibLouis.NET/LibLouis.cs
@@ -8,7 +8,7 @@
namespace LibLouis.NET;
-public class LibLouis : IDisposable
+public class LibLouis
{
///
/// LibLouis loglevels to ILogger logLevels table.
@@ -27,7 +27,13 @@ public class LibLouis : IDisposable
///
/// LibLouis is *NOT* thread safe, so we'll have to use a lock to avoid concurrrent access to native liblouis calls.
///
- private readonly object _lock;
+ ///
+ /// Static, and shared with : the state it protects belongs to the native
+ /// library, not to this instance, so every native call in the assembly has to serialise on the
+ /// same object. Monitor is reentrant, so a logger that calls back in while liblouis is logging
+ /// does not deadlock.
+ ///
+ internal static readonly object NativeLock = new();
///
/// LibLouis can currently use either UCS-4 (1:1 mapping of UTF-32), or UCS-2 (WTF-16 without surrogate pairs),
@@ -48,6 +54,17 @@ public class LibLouis : IDisposable
private string _lastLogMessage = string.Empty;
+ ///
+ /// Roots the delegate behind the function pointer liblouis holds.
+ ///
+ ///
+ /// The interop stub only keeps the delegate alive for the duration of the registration call,
+ /// but liblouis keeps calling the pointer for the rest of the process's life. Without a
+ /// reference here the delegate is collected and the next native log message kills the process
+ /// with "A callback was made on a garbage collected delegate".
+ ///
+ private readonly NativeMethods.LoggingCallback _logCallback;
+
static LibLouis()
{
Instance = new LibLouis();
@@ -55,7 +72,8 @@ static LibLouis()
private LibLouis()
{
- _lock = new object();
+ // unlocked: the type initializer runs single threaded, and no other thread can hold a
+ // reference to the singleton until it has finished, so there is nothing to race with.
CharacterSize = NativeMethods.lou_charSize();
LibLouisStringEncoder = CharacterSize switch
{
@@ -64,19 +82,27 @@ private LibLouis()
_ => throw new NotImplementedException($"Liblouis is a character size of {CharacterSize}!?"),
};
+ _logCallback = LogCallback;
+
// Register managed log callback, so we can give reasonable exception messages.
- NativeMethods.lou_registerLogCallback(LogCallback);
+ // unlocked: same reason - still inside the type initializer.
+ NativeMethods.lou_registerLogCallback(_logCallback);
}
- // https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/unmanaged
- ~LibLouis()
- {
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
- Dispose(disposing: false);
- }
+ // Deliberately no finalizer. lou_free tears down state that is global to the process, while a
+ // finalizer runs per managed instance: in a collectible AssemblyLoadContext it would free the
+ // tables of every other context still using liblouis, from the finalizer thread, outside the
+ // lock. Nothing here owns a handle that leaks if Shutdown is never called.
private ILogger _logger = NullLogger.Instance;
- private bool disposedValue;
+
+ ///
+ /// Read by the guards without the lock, written by under it.
+ ///
+ ///
+ /// Static because what it tracks is the state of the native library, not of this instance.
+ ///
+ private static volatile bool _shutDown;
///
/// ILogger instance LibLouis will log to.
@@ -91,34 +117,64 @@ private void SetLogger(ILogger logger)
{
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
- lock (_lock)
+ // Deliberately usable after disposal: neither call touches anything lou_free released,
+ // and being able to attach a logger while shutting down is worth more than the symmetry.
+ lock (NativeLock)
{
_logger = logger;
NativeMethods.lou_setLogLevel(LogLevel.All);
- NativeMethods.lou_registerLogCallback(LogCallback);
+ NativeMethods.lou_registerLogCallback(_logCallback);
}
}
+ ///
+ /// Called by liblouis, on a native stack.
+ ///
+ ///
+ /// Nothing may be thrown out of here. liblouis has no way to handle a managed exception, and
+ /// letting one unwind through its frames tears the process down.
+ ///
private void LogCallback(LogLevel level, string message)
{
- Microsoft.Extensions.Logging.LogLevel l = LogLevels[level];
- _lastLogMessage = message;
+ try
+ {
+ _lastLogMessage = message;
- if (_logger.IsEnabled(l))
+ // liblouis is free to introduce log levels we have no mapping for.
+ if (!LogLevels.TryGetValue(level, out Microsoft.Extensions.Logging.LogLevel l))
+ {
+ l = Microsoft.Extensions.Logging.LogLevel.Information;
+ }
+
+ if (_logger.IsEnabled(l))
+ {
+ // Passed as an argument, not as the template: liblouis messages contain table
+ // paths and rule text, and a stray brace would otherwise be parsed as a
+ // placeholder.
+ _logger.Log(l, "{LiblouisMessage}", message);
+ }
+ }
+ catch
{
- _logger.Log(l, message);
+ // A logger that throws must not become a native crash.
}
}
///
/// Returns version number of the native liblouis library.
///
+ ///
+ /// Readable after disposal: lou_version returns a compile-time constant and touches nothing
+ /// lou_free released, and version information is worth having while diagnosing a shutdown.
+ ///
public string Version
{
get
{
- string version = NativeMethods.lou_version();
- return version;
+ lock (NativeLock)
+ {
+ return NativeMethods.lou_version();
+ }
}
}
@@ -130,16 +186,18 @@ public string? DataPath
{
get
{
- lock (_lock)
+ lock (NativeLock)
{
+ ThrowIfShutDown();
return NativeMethods.lou_getDataPath();
}
}
set
{
ArgumentException.ThrowIfNullOrWhiteSpace(value, nameof(value));
- lock (_lock)
+ lock (NativeLock)
{
+ ThrowIfShutDown();
NativeMethods.lou_setDataPath(value);
}
}
@@ -161,8 +219,9 @@ public string? DataPath
{
ArgumentException.ThrowIfNullOrWhiteSpace(query, nameof(query));
- lock (_lock)
+ lock (NativeLock)
{
+ ThrowIfShutDown();
return NativeMethods.lou_findTable(query);
}
}
@@ -173,9 +232,17 @@ public string? DataPath
/// tables must be an IEnumerable of file names.
public void IndexTables(IEnumerable tables)
{
- lock (_lock)
+ ArgumentNullException.ThrowIfNull(tables);
+
+ // liblouis walks the array until it reads a null pointer, so it needs a terminator on top
+ // of the table names. Without it, it reads whatever managed memory follows the array and
+ // hands it to _lou_logMessage as a string.
+ string?[] nullTerminated = [.. tables, null];
+
+ lock (NativeLock)
{
- NativeMethods.lou_indexTables(tables.ToArray());
+ ThrowIfShutDown();
+ NativeMethods.lou_indexTables(nullTerminated);
}
}
@@ -188,15 +255,18 @@ public string DotsToCharacters(IEnumerable tableList, string input)
{
ArgumentNullException.ThrowIfNull(input, nameof(input));
+ int length = CountUCSCharacters(input);
+
byte[] inputBuffer = PrepareUCSInputBuffer(input);
- byte[] outputBuffer = PrepareUCSOutputBuffer(input.Length);
+ byte[] outputBuffer = PrepareUCSOutputBuffer(length);
string tables = string.Join(',', tableList);
bool success;
- lock (_lock)
+ lock (NativeLock)
{
- success = NativeMethods.lou_dotsToChar(tables, inputBuffer, outputBuffer, input.Length, TranslationMode.Regular) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_dotsToChar(tables, inputBuffer, outputBuffer, length, TranslationMode.Regular) > 0;
}
if (!success)
@@ -204,7 +274,7 @@ public string DotsToCharacters(IEnumerable tableList, string input)
throw new LibLouisException($"String translation failed: {_lastLogMessage}");
}
- return ConvertUCSOutputBufferToString(outputBuffer, input.Length);
+ return ConvertUCSOutputBufferToString(outputBuffer, length);
}
///
@@ -216,16 +286,19 @@ public string CharactersToDots(IEnumerable tableList, string input)
{
ArgumentNullException.ThrowIfNull(input, nameof(input));
+ int length = CountUCSCharacters(input);
+
byte[] inputBuffer = PrepareUCSInputBuffer(input);
- byte[] outputBuffer = PrepareUCSOutputBuffer(input.Length);
-
+ byte[] outputBuffer = PrepareUCSOutputBuffer(length);
+
string tables = string.Join(',', tableList);
bool success;
- lock (_lock)
+ lock (NativeLock)
{
- success = NativeMethods.lou_charToDots(tables, inputBuffer, outputBuffer, input.Length, TranslationMode.Regular) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_charToDots(tables, inputBuffer, outputBuffer, length, TranslationMode.Regular) > 0;
}
if (!success)
@@ -233,8 +306,7 @@ public string CharactersToDots(IEnumerable tableList, string input)
throw new LibLouisException($"String translation failed: {_lastLogMessage}");
}
- return ConvertUCSOutputBufferToString(outputBuffer, input.Length);
-
+ return ConvertUCSOutputBufferToString(outputBuffer, length);
}
///
@@ -285,18 +357,27 @@ public TranslatedString Translate(
throw new ArgumentException($"{nameof(outputPosition)} parameter must point to an array of integers with at least input length elements.", nameof(outputPosition));
}
- int inputLength = input.Length + 1;
+ // The number of widechars to translate, excluding the NUL terminator, which is what the
+ // header means by inlen and what upstream callers pass. The buffer stays terminated: the
+ // translate functions clamp at the first NUL, so an embedded NUL still ends the input.
+ int inputLength = CountUCSCharacters(input);
int outputBufferLength = outputLength;
byte[] inputBuffer = PrepareUCSInputBuffer(input);
byte[] outputBuffer = PrepareUCSOutputBuffer(outputBufferLength);
+ TypeForm[]? typeFormBuffer = PrepareTypeFormBuffer(formtype, inputLength, outputBufferLength);
+
+ // The cursor arrives as a .NET string index and liblouis wants a widechar index.
+ int[] inputOffsets = Utf16OffsetOfWidechar(input);
+ int widecharCursor = ToWidecharCursor(input, cursorPosition);
string tables = string.Join(',', tableList);
bool success;
- lock (_lock)
+ lock (NativeLock)
{
- success = NativeMethods.lou_translate(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, formtype, spacing, outputPosition, inputPosition, ref cursorPosition, mode) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_translate(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, typeFormBuffer, spacing, outputPosition, inputPosition, ref widecharCursor, mode) > 0;
}
if (!success)
@@ -304,12 +385,18 @@ public TranslatedString Translate(
throw new LibLouisException($"String translation failed: {_lastLogMessage}");
}
+ string output = ConvertUCSOutputBufferToString(outputBuffer, outputLength);
+
+ (int[] mappedOutputPosition, int[] mappedInputPosition, int mappedCursor) =
+ MapPositionsToUtf16(input, output, inputOffsets, outputPosition, inputPosition, widecharCursor);
+
return new TranslatedString
{
- Output = ConvertUCSOutputBufferToString(outputBuffer, outputLength),
- CursorPosition = cursorPosition,
- InputPosition = inputPosition,
- OutputPosition = outputPosition,
+ Output = output,
+ CursorPosition = mappedCursor,
+ InputPosition = mappedInputPosition,
+ OutputPosition = mappedOutputPosition,
+ OutputDots78 = ExtractOutputDots78(typeFormBuffer, outputLength),
};
}
@@ -339,18 +426,23 @@ public string Translate(IEnumerable tableList, string input, int outputL
throw new ArgumentException("Spacing must be the same length as input or null");
}
- int inputLength = input.Length + 1;
+ // The number of widechars to translate, excluding the NUL terminator, which is what the
+ // header means by inlen and what upstream callers pass. The buffer stays terminated: the
+ // translate functions clamp at the first NUL, so an embedded NUL still ends the input.
+ int inputLength = CountUCSCharacters(input);
int outputBufferLength = outputLength;
byte[] inputBuffer = PrepareUCSInputBuffer(input);
byte[] outputBuffer = PrepareUCSOutputBuffer(outputBufferLength);
+ TypeForm[]? typeFormBuffer = PrepareTypeFormBuffer(formtype, inputLength, outputBufferLength);
string tables = string.Join(',', tableList);
bool success;
- lock (_lock)
+ lock (NativeLock)
{
- success = NativeMethods.lou_translateString(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, formtype, spacing, mode) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_translateString(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, typeFormBuffer, spacing, mode) > 0;
}
if (!success)
@@ -410,18 +502,27 @@ public TranslatedString BackTranslate(
throw new ArgumentException($"{nameof(outputPosition)} parameter must point to an array of integers with at least input length elements.", nameof(outputPosition));
}
- int inputLength = input.Length + 1;
+ // The number of widechars to translate, excluding the NUL terminator, which is what the
+ // header means by inlen and what upstream callers pass. The buffer stays terminated: the
+ // translate functions clamp at the first NUL, so an embedded NUL still ends the input.
+ int inputLength = CountUCSCharacters(input);
int outputBufferLength = outputLength;
byte[] inputBuffer = PrepareUCSInputBuffer(input);
byte[] outputBuffer = PrepareUCSOutputBuffer(outputBufferLength);
+ TypeForm[]? typeFormBuffer = PrepareTypeFormBuffer(formtype, inputLength, outputBufferLength);
+
+ // The cursor arrives as a .NET string index and liblouis wants a widechar index.
+ int[] inputOffsets = Utf16OffsetOfWidechar(input);
+ int widecharCursor = ToWidecharCursor(input, cursorPosition);
string tables = string.Join(',', tableList);
bool success;
- lock (_lock)
+ lock (NativeLock)
{
- success = NativeMethods.lou_backTranslate(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, formtype, spacing, outputPosition, inputPosition, ref cursorPosition, mode) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_backTranslate(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, typeFormBuffer, spacing, outputPosition, inputPosition, ref widecharCursor, mode) > 0;
}
if (!success)
@@ -429,12 +530,17 @@ public TranslatedString BackTranslate(
throw new LibLouisException($"String translation failed: {_lastLogMessage}");
}
+ string output = ConvertUCSOutputBufferToString(outputBuffer, outputLength);
+
+ (int[] mappedOutputPosition, int[] mappedInputPosition, int mappedCursor) =
+ MapPositionsToUtf16(input, output, inputOffsets, outputPosition, inputPosition, widecharCursor);
+
return new TranslatedString
{
- Output = ConvertUCSOutputBufferToString(outputBuffer, outputLength),
- CursorPosition = cursorPosition,
- InputPosition = inputPosition,
- OutputPosition = outputPosition,
+ Output = output,
+ CursorPosition = mappedCursor,
+ InputPosition = mappedInputPosition,
+ OutputPosition = mappedOutputPosition,
};
}
@@ -462,18 +568,23 @@ public string BackTranslate(IEnumerable tableList, string input, int out
throw new ArgumentException("Spacing must be the same length as input or null");
}
- int inputLength = input.Length + 1;
+ // The number of widechars to translate, excluding the NUL terminator, which is what the
+ // header means by inlen and what upstream callers pass. The buffer stays terminated: the
+ // translate functions clamp at the first NUL, so an embedded NUL still ends the input.
+ int inputLength = CountUCSCharacters(input);
int outputBufferLength = outputLength;
byte[] inputBuffer = PrepareUCSInputBuffer(input);
byte[] outputBuffer = PrepareUCSOutputBuffer(outputBufferLength);
+ TypeForm[]? typeFormBuffer = PrepareTypeFormBuffer(formtype, inputLength, outputBufferLength);
string tables = string.Join(',', tableList);
bool success;
- lock (_lock)
+ lock (NativeLock)
{
- success = NativeMethods.lou_backTranslateString(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, formtype, spacing, mode) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_backTranslateString(tables, inputBuffer, ref inputLength, outputBuffer, ref outputLength, typeFormBuffer, spacing, mode) > 0;
}
if (!success)
@@ -491,33 +602,260 @@ public string BackTranslate(IEnumerable tableList, string input, int out
/// If it does not, the function does nothing.
///
///
- ///
+ /// The word to hyphenate. Must be shorter than 100 characters.
///
- ///
+ ///
+ /// One character per character of : '1' where the word may be broken,
+ /// '0' where it may not, '2' after an existing hyphen. On a UCS-4 build a non-BMP character
+ /// counts once, so the result can be shorter than .
+ ///
///
public string Hyphenate(IEnumerable tableList, string input, TranslationMode mode)
{
ArgumentNullException.ThrowIfNull(tableList);
- ArgumentNullException.ThrowIfNullOrEmpty(nameof(input));
+ ArgumentException.ThrowIfNullOrEmpty(input);
+
+ int length = CountUCSCharacters(input);
+
+ // liblouis rejects anything from HYPHSTRING characters up, and would otherwise report it
+ // as an ordinary hyphenation failure.
+ if (length >= MaxHyphenationLength)
+ {
+ throw new ArgumentException(
+ $"{nameof(input)} must be shorter than {MaxHyphenationLength} characters.", nameof(input));
+ }
string tables = string.Join(',', tableList);
- string hyphens = new('\0', input.Length + 1);
+
+ // liblouis writes one flag per character plus a NUL terminator into a caller-allocated
+ // char buffer. inlen must not count the terminator: lou_hyphenate memcpy's exactly inlen
+ // widechars rather than stopping at a NUL the way the translate functions do, so an
+ // inlen in the wrong unit reads straight past the input buffer.
+ byte[] hyphens = new byte[length + 1];
byte[] inputBuffer = PrepareUCSInputBuffer(input);
bool success;
-
- lock (_lock)
+
+ lock (NativeLock)
{
- success = NativeMethods.lou_hyphenate(tables, inputBuffer, input.Length + 1, ref hyphens, mode) > 0;
+ ThrowIfShutDown();
+ success = NativeMethods.lou_hyphenate(tables, inputBuffer, length, hyphens, mode) > 0;
}
-
+
if (!success)
{
throw new LibLouisException($"Hyphenation failed {_lastLogMessage}");
}
- return hyphens;
+ // The flags are ASCII digits; the trailing terminator is not part of the result.
+ return Encoding.ASCII.GetString(hyphens, 0, length);
+ }
+
+ ///
+ /// liblouis hyphenates into a fixed 100 character buffer (HYPHSTRING) and refuses any input
+ /// that would not fit.
+ ///
+ private const int MaxHyphenationLength = 100;
+
+ ///
+ /// Copy the caller's typeform values into a buffer that is safe to hand to liblouis.
+ ///
+ ///
+ /// The typeform parameter is in/out: liblouis reads one entry per input character, but on a
+ /// successful translation it writes one entry per *output* cell. A translation that grows the
+ /// text - which the marker tables do routinely - would therefore write past the end of an
+ /// array sized to the input, corrupting the managed heap. We give liblouis a buffer big enough
+ /// for both directions and treat the caller's array as input only.
+ ///
+ private static TypeForm[]? PrepareTypeFormBuffer(TypeForm[]? formtype, int inputLength, int outputLength)
+ {
+ if (formtype is null)
+ {
+ return null;
+ }
+
+ TypeForm[] buffer = new TypeForm[Math.Max(inputLength, outputLength) + 1];
+ formtype.AsSpan(0, Math.Min(formtype.Length, buffer.Length)).CopyTo(buffer);
+
+ return buffer;
+ }
+
+ ///
+ /// Reads the per-cell dot 7/8 information liblouis wrote into the scratch typeform buffer.
+ ///
+ ///
+ /// The write-back half of : on a successful forward
+ /// translation liblouis stores the ASCII character '8' in the slot of every output cell that
+ /// contains dot 7 or dot 8, and '0' otherwise (lou_translateString.c:1330). Those are
+ /// characters smuggled through a formtype array, not TypeForm flag values, which is why this
+ /// converts to booleans instead of exposing the buffer.
+ ///
+ private static bool[]? ExtractOutputDots78(TypeForm[]? typeFormBuffer, int outputLength)
+ {
+ if (typeFormBuffer is null)
+ {
+ return null;
+ }
+
+ bool[] dots = new bool[outputLength];
+
+ for (int k = 0; k < outputLength; k++)
+ {
+ dots[k] = typeFormBuffer[k] == (TypeForm)'8';
+ }
+
+ return dots;
+ }
+
+ ///
+ /// Converts a cursor given as a .NET string index into the widechar index liblouis expects.
+ ///
+ ///
+ /// Negative means "no cursor" to liblouis and is passed through untouched.
+ ///
+ private int ToWidecharCursor(string input, int cursorPosition)
+ {
+ if (cursorPosition < 0 || input.Length == 0)
+ {
+ return cursorPosition;
+ }
+
+ int[] widechars = WidecharOfUtf16Offset(input);
+
+ return widechars[Math.Clamp(cursorPosition, 0, input.Length - 1)];
+ }
+
+ ///
+ /// Rewrites liblouis's widechar-indexed position arrays as UTF-16 indices into the managed
+ /// strings, so every value can be used directly as a string index.
+ ///
+ ///
+ /// liblouis counts in widechars: on a UCS-4 build one widechar is a whole Unicode character,
+ /// while a .NET string counts UTF-16 code units. The two agree for BMP text and diverge from
+ /// the first non-BMP character on, which silently misaligns any caller that treats these
+ /// values as string indices - and the arrays exist for nothing else.
+ ///
+ /// The results are sized to the strings they index rather than to the caller's scratch
+ /// buffers, so OutputPosition has one entry per char of the input and
+ /// InputPosition one per char of the output. No slicing is required to use them.
+ ///
+ /// Both halves of a surrogate pair report the same position, since they are one character.
+ ///
+ private (int[] OutputPosition, int[] InputPosition, int CursorPosition) MapPositionsToUtf16(
+ string input,
+ string output,
+ int[] inputOffsets,
+ int[] outputWidecharPositions,
+ int[] inputWidecharPositions,
+ int widecharCursor)
+ {
+ int[] outputOffsets = Utf16OffsetOfWidechar(output);
+ int[] inputWidechars = WidecharOfUtf16Offset(input);
+ int[] outputWidechars = WidecharOfUtf16Offset(output);
+
+ int lastInputWidechar = Math.Max(inputOffsets.Length - 2, 0);
+ int lastOutputWidechar = Math.Max(outputOffsets.Length - 2, 0);
+
+ int[] outputPosition = new int[input.Length];
+
+ for (int i = 0; i < input.Length; i++)
+ {
+ int widechar = inputWidechars[i];
+
+ int cell = widechar < outputWidecharPositions.Length ? outputWidecharPositions[widechar] : 0;
+
+ outputPosition[i] = outputOffsets[Math.Clamp(cell, 0, lastOutputWidechar)];
+ }
+
+ int[] inputPosition = new int[output.Length];
+
+ for (int t = 0; t < output.Length; t++)
+ {
+ int widechar = outputWidechars[t];
+
+ int character = widechar < inputWidecharPositions.Length ? inputWidecharPositions[widechar] : 0;
+
+ inputPosition[t] = inputOffsets[Math.Clamp(character, 0, lastInputWidechar)];
+ }
+
+ // A negative cursor means "no cursor" to liblouis; leave it alone.
+ int cursorPosition = widecharCursor < 0 || output.Length == 0
+ ? widecharCursor
+ : outputOffsets[Math.Clamp(widecharCursor, 0, lastOutputWidechar)];
+
+ return (outputPosition, inputPosition, cursorPosition);
+ }
+
+ ///
+ /// The UTF-16 offset at which each widechar of starts, with a
+ /// sentinel holding the string's length at the end.
+ ///
+ private int[] Utf16OffsetOfWidechar(string value)
+ {
+ int[] offsets = new int[CountUCSCharacters(value) + 1];
+
+ int widechar = 0;
+
+ for (int i = 0; i < value.Length; widechar++)
+ {
+ offsets[widechar] = i;
+ i += IsSurrogatePairAt(value, i) ? 2 : 1;
+ }
+
+ offsets[widechar] = value.Length;
+
+ return offsets;
+ }
+
+ ///
+ /// The widechar that each UTF-16 offset of belongs to. Both halves of
+ /// a surrogate pair map to the same widechar, because they are one character to liblouis.
+ ///
+ private int[] WidecharOfUtf16Offset(string value)
+ {
+ int[] widechars = new int[value.Length];
+
+ int widechar = 0;
+
+ for (int i = 0; i < value.Length; widechar++)
+ {
+ int width = IsSurrogatePairAt(value, i) ? 2 : 1;
+
+ for (int k = 0; k < width; k++)
+ {
+ widechars[i + k] = widechar;
+ }
+
+ i += width;
+ }
+
+ return widechars;
+ }
+
+ ///
+ /// Whether a surrogate pair - one widechar, two chars - starts at .
+ /// Never true on a UCS-2 build, where a widechar is a UTF-16 code unit.
+ ///
+ private bool IsSurrogatePairAt(string value, int index)
+ {
+ return CharacterSize == 4
+ && char.IsHighSurrogate(value[index])
+ && index + 1 < value.Length
+ && char.IsLowSurrogate(value[index + 1]);
+ }
+
+ ///
+ /// The number of liblouis widechars occupies.
+ ///
+ ///
+ /// Not the same as string.Length on a UCS-4 build: a non-BMP character is one widechar but
+ /// two chars. Lengths handed to liblouis have to be counted in widechars, or they describe a
+ /// longer buffer than the one that was allocated.
+ ///
+ private int CountUCSCharacters(string input)
+ {
+ return LibLouisStringEncoder.GetByteCount(input) / CharacterSize;
}
///
@@ -554,27 +892,59 @@ private string ConvertUCSOutputBufferToString(byte[] outputBuffer, int outputLen
return LibLouisStringEncoder.GetString(outputBuffer, 0, Math.Min(outputLength * CharacterSize, outputBuffer.Length));
}
- protected virtual void Dispose(bool disposing)
+ ///
+ /// Throws if liblouis has already been torn down.
+ ///
+ ///
+ /// Called from inside the lock, immediately before the native call. Checking on the way in
+ /// instead would leave a window for Shutdown to free the tables between check and call.
+ ///
+ /// Not ObjectDisposedException: this type is not disposable, and "cannot access a disposed
+ /// object" would send the reader looking for a Dispose call that does not exist.
+ ///
+ private static void ThrowIfShutDown()
{
- if (!disposedValue)
+ if (_shutDown)
{
- if (disposing)
+ throw new InvalidOperationException(
+ "liblouis has been shut down. LibLouis.Shutdown() frees state that is global to "
+ + "the process and cannot be undone.");
+ }
+ }
+
+ ///
+ /// Frees everything liblouis has allocated. Final: there is no way back.
+ ///
+ ///
+ /// Deliberately a static method rather than IDisposable. lou_free walks and frees the
+ /// translation and display table chains, which are global to the process, so this is teardown
+ /// for the whole application rather than the release of a resource one caller owns. Exposing
+ /// it as IDisposable invited using (LibLouis.Instance), which reads as ordinary
+ /// cleanup and would leave every other consumer in the process unable to translate.
+ ///
+ /// Only worth calling when you need the tables released before the process exits - checking
+ /// for leaks, say. Normal applications should not call it at all: liblouis caches compiled
+ /// tables per table list rather than per call, so nothing accumulates, and process exit
+ /// reclaims it anyway.
+ ///
+ /// Takes the same lock as every other native call. Freeing those chains while another thread
+ /// is translating is a use-after-free, which shows up as anything from a nonsense
+ /// "no mapping for dot pattern" error to a crash.
+ ///
+ /// Calling it more than once does nothing.
+ ///
+ public static void Shutdown()
+ {
+ lock (NativeLock)
+ {
+ if (_shutDown)
{
- // Dispose managed state (managed objects)
+ return;
}
- // Free unmanaged resources (unmanaged objects) and override finalizer
NativeMethods.lou_free();
- // Set large fields to null
- disposedValue = true;
+ _shutDown = true;
}
}
-
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
}
diff --git a/LibLouis.NET/Logging.cs b/LibLouis.NET/Logging.cs
index a4a077d..e3449e1 100644
--- a/LibLouis.NET/Logging.cs
+++ b/LibLouis.NET/Logging.cs
@@ -1,10 +1,29 @@
-namespace LibLouis.NET;
+using System;
+namespace LibLouis.NET;
+
+///
+/// These change the same global liblouis state that uses, so they take the
+/// same lock. Setting the callback or the log level while another thread is inside a translation
+/// is otherwise an unsynchronised write to state liblouis reads as it logs.
+///
public static class Logging
{
+ ///
+ /// Roots the delegate behind the function pointer liblouis holds. Callers routinely pass a
+ /// method group, which would otherwise be collected while liblouis still calls it.
+ ///
+ private static NativeMethods.LoggingCallback? _callback;
+
public static void SetCallback(NativeMethods.LoggingCallback value)
{
- NativeMethods.lou_registerLogCallback(value);
+ ArgumentNullException.ThrowIfNull(value);
+
+ lock (LibLouis.NativeLock)
+ {
+ _callback = value;
+ NativeMethods.lou_registerLogCallback(_callback);
+ }
}
private static LogLevel _logLevel = LogLevel.Off;
@@ -13,12 +32,18 @@ public static LogLevel LogLevel
{
get
{
- return _logLevel;
+ lock (LibLouis.NativeLock)
+ {
+ return _logLevel;
+ }
}
set
{
- _logLevel = value;
- NativeMethods.lou_setLogLevel(value);
+ lock (LibLouis.NativeLock)
+ {
+ _logLevel = value;
+ NativeMethods.lou_setLogLevel(value);
+ }
}
}
diff --git a/LibLouis.NET/NativeMethod.cs b/LibLouis.NET/NativeMethod.cs
index 4eaf17f..95f55df 100644
--- a/LibLouis.NET/NativeMethod.cs
+++ b/LibLouis.NET/NativeMethod.cs
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
namespace LibLouis.NET;
@@ -12,7 +13,8 @@ public static partial class NativeMethods
///
/// LibLouis version.
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
- [LibraryImport("liblouis", EntryPoint = "lou_version", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(UTF8StringNoFreeMarshaller))]
+ [LibraryImport("liblouis", EntryPoint = "lou_version")]
+ [return: MarshalUsing(typeof(UTF8StringNoFreeMarshaller))]
internal static partial string lou_version();
///
@@ -130,13 +132,22 @@ internal static partial int lou_backTranslateString(
///
/// Contains a hyphenation table.
/// length of the character string in inbuf.
- /// inlen is the length of the character string in inbuf
- /// array of characters and must be of size inlen + 1 (to account for the NULL terminator).
+ ///
+ /// The number of characters in inbuf. Unlike the translate functions, lou_hyphenate does not
+ /// stop at a NUL: it copies exactly inlen characters, so this must not count the terminator.
+ /// It must also be less than 100 (HYPHSTRING), or liblouis refuses the call.
+ ///
+ ///
+ /// Caller-allocated output buffer of at least inlen + 1 bytes. liblouis writes one ASCII
+ /// '0' / '1' / '2' per character plus a NUL terminator. It is a plain char buffer, so it must
+ /// be marshalled as a byte array - a string would pass a pointer to a pointer and liblouis
+ /// would write over the marshalling stub's own stack.
+ ///
///
/// 0 if error, 1 if success.
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[LibraryImport("liblouis", EntryPoint = "lou_hyphenate", StringMarshalling = StringMarshalling.Utf8)]
- internal static partial int lou_hyphenate(string tableList, byte[] inbuf, int inlen, ref string hyphens, TranslationMode mode);
+ internal static partial int lou_hyphenate(string tableList, byte[] inbuf, int inlen, byte[] hyphens, TranslationMode mode);
///
/// This function enables you to compile a table entry on the fly at run-time.
@@ -174,25 +185,48 @@ internal static partial int lou_backTranslateString(
[LibraryImport("liblouis", EntryPoint = "lou_registerLogCallback")]
internal static partial void lou_registerLogCallback(LoggingCallback callback);
+ ///
+ /// A pointer into static storage inside liblouis, or if the path was
+ /// never set. Must not be freed.
+ ///
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
- [LibraryImport("liblouis", EntryPoint = "lou_getDataPath", StringMarshalling = StringMarshalling.Utf8)]
- internal static partial string lou_getDataPath();
+ [LibraryImport("liblouis", EntryPoint = "lou_getDataPath")]
+ [return: MarshalUsing(typeof(UTF8StringNoFreeMarshaller))]
+ internal static partial string? lou_getDataPath();
+ ///
+ /// A pointer into static storage inside liblouis, or if the path was
+ /// rejected. Must not be freed.
+ ///
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[LibraryImport("liblouis", EntryPoint = "lou_setDataPath", StringMarshalling = StringMarshalling.Utf8)]
- internal static partial string lou_setDataPath(string path);
+ [return: MarshalUsing(typeof(UTF8StringNoFreeMarshaller))]
+ internal static partial string? lou_setDataPath(string path);
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[LibraryImport("liblouis", EntryPoint = "lou_checkTable", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int lou_checkTable(string tableList);
+ ///
+ /// Parses, analyzes and indexes the given tables.
+ ///
+ ///
+ /// Must be NULL terminated: liblouis walks the array until it reads a null pointer, so the
+ /// final element has to be .
+ ///
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[LibraryImport("liblouis", EntryPoint = "lou_indexTables", StringMarshalling = StringMarshalling.Utf8)]
- internal static partial void lou_indexTables(string[] tables);
+ internal static partial void lou_indexTables(string?[] tables);
+ ///
+ /// The best matching table name, or when there is no match. liblouis
+ /// documents this as the caller's to free, but the memory comes from liblouis's own C runtime
+ /// - see for why we leak it instead.
+ ///
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[LibraryImport("liblouis", EntryPoint = "lou_findTable", StringMarshalling = StringMarshalling.Utf8)]
- internal static partial string lou_findTable(string query);
+ [return: MarshalUsing(typeof(UTF8StringNoFreeMarshaller))]
+ internal static partial string? lou_findTable(string query);
[DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
[LibraryImport("liblouis", EntryPoint = "lou_compileString", StringMarshalling = StringMarshalling.Utf8)]
diff --git a/LibLouis.NET/TranslatedString.cs b/LibLouis.NET/TranslatedString.cs
index b043b02..4e82eab 100644
--- a/LibLouis.NET/TranslatedString.cs
+++ b/LibLouis.NET/TranslatedString.cs
@@ -3,10 +3,45 @@
public class TranslatedString
{
public required string Output { get; set; }
-
+
+ ///
+ /// For each char of the input, the index into it translated to. One entry
+ /// per char, so OutputPosition.Length equals the input's length and no slicing is
+ /// needed. Both halves of a surrogate pair report the same position.
+ ///
+ ///
+ /// A UTF-16 index, usable directly against the strings. liblouis reports these in widechars -
+ /// whole characters on a UCS-4 build - which agrees with UTF-16 only for BMP text; the
+ /// wrapper translates them. This is not the array passed in, which stays as liblouis wrote it.
+ ///
public required int[] OutputPosition { get; set; }
-
+
+ ///
+ /// For each char of , the index into the input it came from. One entry per
+ /// char, so InputPosition.Length equals Output.Length.
+ ///
+ ///
+ /// A UTF-16 index, on the same terms as . Values always address
+ /// the start of a character, never the trailing half of a surrogate pair.
+ ///
public required int[] InputPosition { get; set; }
-
+
+ ///
+ /// Where the cursor ended up, as an index into . Negative when the
+ /// translation was given no cursor.
+ ///
public required int CursorPosition { get; set; }
+
+ ///
+ /// Per output cell, whether liblouis reported the cell as containing dot 7 or dot 8.
+ /// when the translation ran without a formtype array, because liblouis
+ /// only computes this when one is supplied.
+ ///
+ ///
+ /// This is the write-back half of the native typeform parameter. liblouis writes it per
+ /// *output* cell, which is why it cannot go into the caller's input-sized formtype array -
+ /// that write is exactly the buffer overrun the wrapper exists to prevent. Forward
+ /// translation only: back-translation zero-fills the buffer and reports nothing.
+ ///
+ public bool[]? OutputDots78 { get; set; }
}
diff --git a/LibLouis.NET/UTF8StringNoFreeMarshaller.cs b/LibLouis.NET/UTF8StringNoFreeMarshaller.cs
index 17fa844..0b5e470 100644
--- a/LibLouis.NET/UTF8StringNoFreeMarshaller.cs
+++ b/LibLouis.NET/UTF8StringNoFreeMarshaller.cs
@@ -1,49 +1,43 @@
-using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
-using System.Text;
namespace LibLouis.NET;
-[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(UTF8StringNoFreeMarshaller))]
-public unsafe static class UTF8StringNoFreeMarshaller
+///
+/// Marshals a UTF-8 string that liblouis owns, without freeing it.
+///
+///
+/// Several liblouis functions return a char * the caller must not release: lou_version,
+/// lou_getDataPath and lou_setDataPath all hand back a pointer into static storage inside the
+/// library. The default UTF-8 marshalling frees whatever the callee returned, which for those
+/// pointers aborts the process ("pointer being freed was not allocated").
+///
+/// It is deliberately restricted to - return
+/// values and out parameters. Not freeing is only correct for memory we did not allocate;
+/// applying it to an input parameter would leak the buffer allocated for every call, so
+/// parameters keep using the built-in .
+///
+/// liblouis also has functions whose result the caller *is* expected to free (lou_findTable,
+/// lou_findTables, lou_getTableInfo, lou_listTables). Those use this marshaller too: the Windows
+/// binaries are built with mingw-w64 and allocate from msvcrt.dll while .NET frees through
+/// ucrtbase.dll, so releasing that memory from managed code would corrupt the heap. Leaking a
+/// bounded number of small strings is the safer trade.
+///
+[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedOut, typeof(UTF8StringNoFreeMarshaller))]
+public static unsafe class UTF8StringNoFreeMarshaller
{
- public const byte NullTerminator = (byte)0;
-
- public static byte* ConvertToUnmanaged(string? managedString)
- {
- if (managedString is null)
- {
- return null;
- }
-
- int unmanagedLength = Encoding.UTF8.GetByteCount(managedString) + 1;
- byte* bufferPointer = (byte*)NativeMemory.Alloc((nuint)unmanagedLength);
- Span byteSpan = new(bufferPointer, unmanagedLength);
-
- byteSpan = Encoding.UTF8.GetBytes(managedString);
- byteSpan[^1] = NullTerminator;
-
- return bufferPointer;
- }
-
-
+ ///
+ /// Copies the NUL terminated UTF-8 string at into a managed string.
+ ///
public static string? ConvertToManaged(byte* unmanaged)
{
- if (unmanaged == null)
- {
- return null;
- }
-
- Span stringSpan = new(unmanaged, int.MaxValue);
- int length = stringSpan.IndexOf(NullTerminator);
-
- return Encoding.UTF8.GetString(unmanaged, length);
+ return Marshal.PtrToStringUTF8((nint)unmanaged);
}
-
+ ///
+ /// Deliberately does nothing: the string belongs to liblouis.
+ ///
public static void Free(byte* unmanaged)
{
- // Do nothing, not caller's responsiblity to free it.
}
}