{
  "id": "P126",
  "slug": "outline-writer",
  "key": "P126-outline-writer",
  "title": "Outline writer",
  "summary": "An outline written as an indented triple-quoted string, read into a tree and written out as bullets, numbers and wrapped lines with a hanging indent; cut to a depth, a with block keeps the indentation where keeping it by hand drifts.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P126 - Outline writer\n\nAn outline written in the program as an indented triple-quoted string,\nread into a tree, and written out as bullets, as numbered items, and\nwrapped to a width with each line after the first lined up under its\ntext.\n\n- `main.eml` - the menu, the source string, the three ways of writing\n  the outline, cutting it to a depth, and adding items\n- `outline.eml` - the source, splitting and dedenting its lines, reading\n  them into a tree, the writer and its with block, and the renderings\n\nHow each part works:\n\n- As in the corpus case `multiline-help-text`, a triple-quoted string\n  keeps what the source layout put in it: here an empty line after the\n  opening quotes, an empty one before the closing quotes, and 4 spaces\n  shared by every line. Reading it drops the blank lines at the ends and\n  takes the shared spaces off; then each level is 2 more spaces than the\n  one above. An odd indentation, or one more than a level deeper, is\n  refused with its line number.\n- Wrapping puts as many words on a line as fit beside the item's number;\n  a word longer than the room left goes on a line of its own and runs\n  past the width, and the program counts those lines.\n- As in the corpus case `indented-report-writer`, the writer keeps its\n  depth with a with block, which steps back on every way out. Cutting the\n  outline to a depth returns early under each item whose children are\n  cut. Kept by hand instead, each early return skips the step back and\n  leaves every later line one more step too far in.\n\nWhat is checked: menu choices 1 to 7; a width from 20 to 80 and a depth\nfrom 0 to 3; an item's number such as 1.2, or 0 for the top, and a text\nof up to 120 characters. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the source string, the bullets and the numbered items;\n- wrapped at 40, and at 20 (two words longer than the room);\n- cut to depth 1, where the depth by hand drifts, and to depth 3, where\n  nothing is cut and the two agree;\n- Tell Splice added under 1.3 and Rest at the top, and the numbers again.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- widths: a cancel, 19, 81 and x; depths: a cancel, 4 and -1;\n- items: a cancel, 3, 1.9, 1..2, a and 1.1.1.1, then under 1 a cancel\n  and 121 characters, and Sub-step under 1.1.1, cut to depth 3.\n\nBuilt on the verified corpus cases `indented-report-writer` (a with block\nthat keeps the shape of the output) and `multiline-help-text` (what a\ntriple-quoted string captures from the source layout).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P126 outline writer: the outline's source string, bullets, numbers,\n# wrapping to a width, cutting to a depth with and without a with block,\n# and adding items.\nimport outline\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef say(lines):\n    for s in lines:\n        s ^0\n\ndef show_source():\n    outline.SOURCE => s\n    outline.split_lines(s) => lines\n    (\"The source string has \" + str(len(s)) + \" characters in \" + str(len(lines)) + \" lines (| marks where each line starts):\") ^0\n    for k in [0:len(lines) - 1]:\n        (\"  \" + lpad(str(k + 1), 2) + \" |\" + lines[k]) ^0\n    outline.dedent(lines) => d\n    (\"Reading it: \" + str(d[1]) + \" blank lines at the ends dropped (the empty line right after the opening quotes, and the empty one before the closing quotes), and the \" + str(d[2]) + \" spaces every line shares taken off.\") ^0\n\ndef tree_of():\n    outline.dedent(outline.split_lines(outline.SOURCE))[0] => lines\n    return outline.parse(lines)[0]\n\ndef ask_int(prompt, low, high):\n    plain(input(prompt)) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return 0 - 1\n    if len(t) > 2:\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n        return 0 - 1\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n            return 0 - 1\n    if int(t) < low or int(t) > high:\n        (\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\") ^0\n        return 0 - 1\n    return int(t)\n\ndef wrapped(tree):\n    ask_int(\"width (20 to 80)> \", 20, 80) => w\n    if w < 0:\n        return\n    outline.numbered(tree, w) => lines\n    (\"Wrapped at \" + str(w) + \", each line after the first lined up under its text:\") ^0\n    \"+\" + \"-\" * w + \"+\" => ruler\n    ruler ^0\n    0 => over\n    for s in lines:\n        if len(s) > w:\n            over + 1 => over\n            (s + \"   <- past the width\") ^0\n        else:\n            s ^0\n    ruler ^0\n    if over == 1:\n        \"1 line runs past the width: a word longer than the room left beside its number.\" ^0\n    elif over > 1:\n        (str(over) + \" lines run past the width: words longer than the room left beside their numbers.\") ^0\n\ndef cut(tree):\n    ask_int(\"depth (0 to 3)> \", 0, 3) => d\n    if d < 0:\n        return\n    outline.to_depth(tree, d, True) => a\n    outline.to_depth(tree, d, False) => b\n    \"With a with block:\" ^0\n    say(a)\n    \"Keeping the depth by hand, an early return skipping the step back:\" ^0\n    say(b)\n    0 => differ\n    for k in [0:len(a) - 1]:\n        if a[k] != b[k]:\n            differ + 1 => differ\n    if differ == 0:\n        \"The two agree: nothing was cut, so no early return happened.\" ^0\n    else:\n        (str(differ) + \" of the \" + str(len(a)) + \" lines differ: each early return leaves every later line by hand one more step too far in.\") ^0\n\ndef find_path(tree, t):\n    # the list of children under a path such as 1.2, or [] with ok False\n    if t == \"0\":\n        return [True, []]\n    [] => nums\n    \"\" => w\n    for c in t + \".\":\n        if c == \".\":\n            if w == \"\" or len(w) > 2:\n                return [False, []]\n            nums + [int(w)] => nums\n            \"\" => w\n        elif c >= \"0\" and c <= \"9\":\n            w + c => w\n        else:\n            return [False, []]\n    tree => level\n    for n in nums:\n        if n < 1 or n > len(level):\n            return [False, []]\n        level[n - 1][1] => level\n    return [True, nums]\n\ndef add_item(tree):\n    plain(input(\"under (an item's number such as 1.2, or 0 for the top)> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return tree\n    find_path(tree, t) => p\n    if not p[0]:\n        \"Type the number of an item, such as 1 or 1.2, or 0.\" ^0\n        return tree\n    plain(input(\"text> \")) => text\n    if text == \"\":\n        \"Cancelled.\" ^0\n        return tree\n    if len(text) > 120:\n        \"Up to 120 characters.\" ^0\n        return tree\n    add_at(tree, p[1], text) => out\n    if len(p[1]) == 0:\n        (\"Added at the top: \" + text) ^0\n    else:\n        (\"Added under \" + t + \": \" + text) ^0\n    return out\n\ndef add_at(level, nums, text):\n    if len(nums) == 0:\n        return level + [[text, []]]\n    [] => out\n    for i in [0:len(level) - 1]:\n        if i == nums[0] - 1:\n            out + [[level[i][0], add_at(level[i][1], nums[1:len(nums)], text)]] => out\n        else:\n            out + [level[i]] => out\n    return out\n\ntree_of() => tree\n\n\"== Outline writer ==\" ^0\n\"An outline written as an indented string, read into a tree, and written out as bullets, as numbers, and wrapped to a width.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_source()\n    elif choice == \"2\":\n        say(outline.bullets(tree))\n    elif choice == \"3\":\n        say(outline.numbered(tree, 0))\n    elif choice == \"4\":\n        wrapped(tree)\n    elif choice == \"5\":\n        cut(tree)\n    elif choice == \"6\":\n        add_item(tree) => tree\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import outline\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef say(lines):\n    for s in lines:\n        print(s)\n\ndef show_source():\n    s = outline.SOURCE\n    lines = outline.split_lines(s)\n    print(\"The source string has \" + str(len(s)) + \" characters in \" + str(len(lines)) + \" lines (| marks where each line starts):\")\n    for k in range(0, len(lines)):\n        print(\"  \" + lpad(str(k + 1), 2) + \" |\" + lines[k])\n    d = outline.dedent(lines)\n    print(\"Reading it: \" + str(d[1]) + \" blank lines at the ends dropped (the empty line right after the opening quotes, and the empty one before the closing quotes), and the \" + str(d[2]) + \" spaces every line shares taken off.\")\n\ndef tree_of():\n    lines = outline.dedent(outline.split_lines(outline.SOURCE))[0]\n    return outline.parse(lines)[0]\n\ndef ask_int(prompt, low, high):\n    t = plain(input(prompt))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return 0 - 1\n    if len(t) > 2:\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n        return 0 - 1\n    for c in t:\n        if c < \"0\" or c > \"9\":\n            print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n            return 0 - 1\n    if int(t) < low or int(t) > high:\n        print(\"Type a whole number from \" + str(low) + \" to \" + str(high) + \".\")\n        return 0 - 1\n    return int(t)\n\ndef wrapped(tree):\n    w = ask_int(\"width (20 to 80)> \", 20, 80)\n    if w < 0:\n        return\n    lines = outline.numbered(tree, w)\n    print(\"Wrapped at \" + str(w) + \", each line after the first lined up under its text:\")\n    ruler = \"+\" + \"-\" * w + \"+\"\n    print(ruler)\n    over = 0\n    for s in lines:\n        if len(s) > w:\n            over = over + 1\n            print(s + \"   <- past the width\")\n        else:\n            print(s)\n    print(ruler)\n    if over == 1:\n        print(\"1 line runs past the width: a word longer than the room left beside its number.\")\n    elif over > 1:\n        print(str(over) + \" lines run past the width: words longer than the room left beside their numbers.\")\n\ndef cut(tree):\n    d = ask_int(\"depth (0 to 3)> \", 0, 3)\n    if d < 0:\n        return\n    a = outline.to_depth(tree, d, True)\n    b = outline.to_depth(tree, d, False)\n    print(\"With a with block:\")\n    say(a)\n    print(\"Keeping the depth by hand, an early return skipping the step back:\")\n    say(b)\n    differ = 0\n    for k in range(0, len(a)):\n        if a[k] != b[k]:\n            differ = differ + 1\n    if differ == 0:\n        print(\"The two agree: nothing was cut, so no early return happened.\")\n    else:\n        print(str(differ) + \" of the \" + str(len(a)) + \" lines differ: each early return leaves every later line by hand one more step too far in.\")\n\ndef find_path(tree, t):\n    if t == \"0\":\n        return [True, []]\n    nums = []\n    w = \"\"\n    for c in t + \".\":\n        if c == \".\":\n            if w == \"\" or len(w) > 2:\n                return [False, []]\n            nums = nums + [int(w)]\n            w = \"\"\n        elif c >= \"0\" and c <= \"9\":\n            w = w + c\n        else:\n            return [False, []]\n    level = tree\n    for n in nums:\n        if n < 1 or n > len(level):\n            return [False, []]\n        level = level[n - 1][1]\n    return [True, nums]\n\ndef add_item(tree):\n    t = plain(input(\"under (an item's number such as 1.2, or 0 for the top)> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return tree\n    p = find_path(tree, t)\n    if not p[0]:\n        print(\"Type the number of an item, such as 1 or 1.2, or 0.\")\n        return tree\n    text = plain(input(\"text> \"))\n    if text == \"\":\n        print(\"Cancelled.\")\n        return tree\n    if len(text) > 120:\n        print(\"Up to 120 characters.\")\n        return tree\n    out = add_at(tree, p[1], text)\n    if len(p[1]) == 0:\n        print(\"Added at the top: \" + text)\n    else:\n        print(\"Added under \" + t + \": \" + text)\n    return out\n\ndef add_at(level, nums, text):\n    if len(nums) == 0:\n        return level + [[text, []]]\n    out = []\n    for i in range(0, len(level)):\n        if i == nums[0] - 1:\n            out = out + [[level[i][0], add_at(level[i][1], nums[1:len(nums)], text)]]\n        else:\n            out = out + [level[i]]\n    return out\n\ntree = tree_of()\nprint(\"== Outline writer ==\")\nprint(\"An outline written as an indented string, read into a tree, and written out as bullets, as numbers, and wrapped to a width.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_source()\n    elif choice == \"2\":\n        say(outline.bullets(tree))\n    elif choice == \"3\":\n        say(outline.numbered(tree, 0))\n    elif choice == \"4\":\n        wrapped(tree)\n    elif choice == \"5\":\n        cut(tree)\n    elif choice == \"6\":\n        tree = add_item(tree)\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "outline.eml",
      "eml": "# P126 outline writer - an outline written as an indented triple-quoted\n# string, read into a tree, and written out as bullets, as numbered items,\n# and wrapped to a width with a hanging indent. As in the corpus case\n# multiline-help-text, the string keeps the newline after its opening\n# quotes and the indentation of its lines, and both have to be taken off.\n# As in indented-report-writer, the depth of the output is kept by a with\n# block, so it comes back down on every way out, early returns included.\n#\n# A node is [text, children].\n\n\"\"\"\n    Release plan\n      Build\n        Compile the interpreter and the transpiler for every platform we ship\n        Run the whole test suite\n      Check\n        Read every golden line by line\n        Cross-check each claim a second way, written separately\n      Publish\n        Build the site\n        Deploy\n    Afterwards\n      Write the journal\n\"\"\" => SOURCE\n\ndef split_lines(text):\n    [] => out\n    \"\" => cur\n    for ch in text:\n        if ch == \"\\n\":\n            out + [cur] => out\n            \"\" => cur\n        else:\n            cur + ch => cur\n    return out + [cur]\n\ndef lead(s):\n    0 => n\n    while n < len(s) and s[n] == \" \":\n        n + 1 => n\n    return n\n\ndef blank(s):\n    return lead(s) == len(s)\n\ndef dedent(lines):\n    # [lines with blank ones at either end dropped, the common indentation\n    #  taken off; how many blank lines were dropped; the indentation]\n    0 => a\n    len(lines) => b\n    while a < b and blank(lines[a]):\n        a + 1 => a\n    while b > a and blank(lines[b - 1]):\n        b - 1 => b\n    0 - 1 => common\n    for k in [a:b - 1]:\n        if not blank(lines[k]):\n            if common < 0 or lead(lines[k]) < common:\n                lead(lines[k]) => common\n    [] => out\n    for k in [a:b - 1]:\n        if blank(lines[k]):\n            out + [\"\"] => out\n        else:\n            out + [lines[k][common:len(lines[k])]] => out\n    return [out, a + len(lines) - b, max2(common, 0)]\n\ndef max2(x, y):\n    if x > y:\n        return x\n    return y\n\ndef parse(lines):\n    # [tree, \"\"] or [[], what is wrong]; each level is 2 more spaces than\n    # the one above\n    [] => roots\n    [] => stack\n    for k in [0:len(lines) - 1]:\n        lines[k] => s\n        if not blank(s):\n            lead(s) => n\n            if n % 2 == 1:\n                return [[], \"line \" + str(k + 1) + \" is indented by an odd number of spaces\"]\n            n / 2 => d\n            int(d) => d\n            if d > len(stack):\n                return [[], \"line \" + str(k + 1) + \" is indented more than one level deeper than the line above\"]\n            [s[n:len(s)], []] => node\n            stack[0:d] => stack\n            if d == 0:\n                roots + [node] => roots\n            else:\n                stack[d - 1][1] + [node] => stack[d - 1][1]\n            stack + [node] => stack\n    return [roots, \"\"]\n\nclass Indent:\n    def __init__(self, writer):\n        writer => self.writer\n    def __enter__(self):\n        self.writer.depth + 1 => self.writer.depth\n        return self.writer\n    def __exit__(self, exc_type, exc_value, tb):\n        self.writer.depth - 1 => self.writer.depth\n        return False\n\nclass Writer:\n    def __init__(self):\n        0 => self.depth\n        [] => self.lines\n    def line(self, text):\n        self.lines + [\"  \" * self.depth + text] => self.lines\n    def block(self):\n        return Indent(self)\n\ndef bullets(tree):\n    Writer() => w\n    for node in tree:\n        bullet(w, node)\n    return w.lines\n\ndef bullet(w, node):\n    [\"-\", \"*\", \"+\"] => marks\n    w.line(marks[w.depth % 3] + \" \" + node[0])\n    for child in node[1]:\n        with w.block():\n            bullet(w, child)\n\ndef numbered(tree, width):\n    # numbered items, wrapped to width (0 for no wrapping), each line after\n    # the first lined up under the text\n    [] => out\n    for i in [0:len(tree) - 1]:\n        number(tree[i], str(i + 1), 0, width) => more\n        out + more => out\n    return out\n\ndef number(node, label, depth, width):\n    \"  \" * depth + label + \" \" => head\n    wrap(node[0], width - len(head)) => parts\n    [head + parts[0]] => out\n    for k in [1:len(parts) - 1]:\n        out + [\" \" * len(head) + parts[k]] => out\n    for i in [0:len(node[1]) - 1]:\n        number(node[1][i], label + \".\" + str(i + 1), depth + 1, width) => more\n        out + more => out\n    return out\n\ndef wrap(text, room):\n    # the text in lines of at most room characters, a word longer than that\n    # on a line of its own; room 0 or less: one line\n    if room <= 0:\n        return [text]\n    [] => out\n    \"\" => cur\n    \"\" => word\n    for ch in text + \" \":\n        if ch == \" \":\n            if word != \"\":\n                if cur == \"\":\n                    word => cur\n                elif len(cur) + 1 + len(word) <= room:\n                    cur + \" \" + word => cur\n                else:\n                    out + [cur] => out\n                    word => cur\n            \"\" => word\n        else:\n            word + ch => word\n    return out + [cur]\n\ndef to_depth(tree, most, with_block):\n    # the outline to a depth, the children below it summed up; with_block\n    # False keeps the depth by hand, and an early return skips the step back\n    Writer() => w\n    for node in tree:\n        if with_block:\n            cut_with(w, node, 0, most)\n        else:\n            cut_by_hand(w, node, 0, most)\n    return w.lines\n\ndef cut_with(w, node, depth, most):\n    w.line(node[0])\n    with w.block():\n        for child in node[1]:\n            if depth + 1 > most:\n                w.line(\"(\" + str(len(node[1])) + \" more)\")\n                return\n            cut_with(w, child, depth + 1, most)\n\ndef cut_by_hand(w, node, depth, most):\n    w.line(node[0])\n    w.depth + 1 => w.depth\n    for child in node[1]:\n        if depth + 1 > most:\n            w.line(\"(\" + str(len(node[1])) + \" more)\")\n            return\n        cut_by_hand(w, child, depth + 1, most)\n    w.depth - 1 => w.depth\n",
      "python": "SOURCE = \"\\n    Release plan\\n      Build\\n        Compile the interpreter and the transpiler for every platform we ship\\n        Run the whole test suite\\n      Check\\n        Read every golden line by line\\n        Cross-check each claim a second way, written separately\\n      Publish\\n        Build the site\\n        Deploy\\n    Afterwards\\n      Write the journal\\n\"\n\ndef split_lines(text):\n    out = []\n    cur = \"\"\n    for ch in text:\n        if ch == \"\\n\":\n            out = out + [cur]\n            cur = \"\"\n        else:\n            cur = cur + ch\n    return out + [cur]\n\ndef lead(s):\n    n = 0\n    while n < len(s) and s[n] == \" \":\n        n = n + 1\n    return n\n\ndef blank(s):\n    return lead(s) == len(s)\n\ndef dedent(lines):\n    a = 0\n    b = len(lines)\n    while a < b and blank(lines[a]):\n        a = a + 1\n    while b > a and blank(lines[b - 1]):\n        b = b - 1\n    common = 0 - 1\n    for k in range(a, b):\n        if not blank(lines[k]):\n            if common < 0 or lead(lines[k]) < common:\n                common = lead(lines[k])\n    out = []\n    for k in range(a, b):\n        if blank(lines[k]):\n            out = out + [\"\"]\n        else:\n            out = out + [lines[k][common:len(lines[k])]]\n    return [out, a + len(lines) - b, max2(common, 0)]\n\ndef max2(x, y):\n    if x > y:\n        return x\n    return y\n\ndef parse(lines):\n    roots = []\n    stack = []\n    for k in range(0, len(lines)):\n        s = lines[k]\n        if not blank(s):\n            n = lead(s)\n            if n % 2 == 1:\n                return [[], \"line \" + str(k + 1) + \" is indented by an odd number of spaces\"]\n            d = n / 2\n            d = int(d)\n            if d > len(stack):\n                return [[], \"line \" + str(k + 1) + \" is indented more than one level deeper than the line above\"]\n            node = [s[n:len(s)], []]\n            stack = stack[0:d]\n            if d == 0:\n                roots = roots + [node]\n            else:\n                stack[d - 1][1] = stack[d - 1][1] + [node]\n            stack = stack + [node]\n    return [roots, \"\"]\n\nclass Indent:\n    def __init__(self, writer):\n        self.writer = writer\n    def __enter__(self):\n        self.writer.depth = self.writer.depth + 1\n        return self.writer\n    def __exit__(self, exc_type, exc_value, tb):\n        self.writer.depth = self.writer.depth - 1\n        return False\n\nclass Writer:\n    def __init__(self):\n        self.depth = 0\n        self.lines = []\n    def line(self, text):\n        self.lines = self.lines + [\"  \" * self.depth + text]\n    def block(self):\n        return Indent(self)\n\ndef bullets(tree):\n    w = Writer()\n    for node in tree:\n        bullet(w, node)\n    return w.lines\n\ndef bullet(w, node):\n    marks = [\"-\", \"*\", \"+\"]\n    w.line(marks[w.depth % 3] + \" \" + node[0])\n    for child in node[1]:\n        with w.block():\n            bullet(w, child)\n\ndef numbered(tree, width):\n    out = []\n    for i in range(0, len(tree)):\n        more = number(tree[i], str(i + 1), 0, width)\n        out = out + more\n    return out\n\ndef number(node, label, depth, width):\n    head = \"  \" * depth + label + \" \"\n    parts = wrap(node[0], width - len(head))\n    out = [head + parts[0]]\n    for k in range(1, len(parts)):\n        out = out + [\" \" * len(head) + parts[k]]\n    for i in range(0, len(node[1])):\n        more = number(node[1][i], label + \".\" + str(i + 1), depth + 1, width)\n        out = out + more\n    return out\n\ndef wrap(text, room):\n    if room <= 0:\n        return [text]\n    out = []\n    cur = \"\"\n    word = \"\"\n    for ch in text + \" \":\n        if ch == \" \":\n            if word != \"\":\n                if cur == \"\":\n                    cur = word\n                elif len(cur) + 1 + len(word) <= room:\n                    cur = cur + \" \" + word\n                else:\n                    out = out + [cur]\n                    cur = word\n            word = \"\"\n        else:\n            word = word + ch\n    return out + [cur]\n\ndef to_depth(tree, most, with_block):\n    w = Writer()\n    for node in tree:\n        if with_block:\n            cut_with(w, node, 0, most)\n        else:\n            cut_by_hand(w, node, 0, most)\n    return w.lines\n\ndef cut_with(w, node, depth, most):\n    w.line(node[0])\n    with w.block():\n        for child in node[1]:\n            if depth + 1 > most:\n                w.line(\"(\" + str(len(node[1])) + \" more)\")\n                return\n            cut_with(w, child, depth + 1, most)\n\ndef cut_by_hand(w, node, depth, most):\n    w.line(node[0])\n    w.depth = w.depth + 1\n    for child in node[1]:\n        if depth + 1 > most:\n            w.line(\"(\" + str(len(node[1])) + \" more)\")\n            return\n        cut_by_hand(w, child, depth + 1, most)\n    w.depth = w.depth - 1\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\n19\n4\n81\n4\nx\n5\n\n5\n4\n5\n-1\n6\n\n6\n3\n6\n1.9\n6\n1..2\n6\na\n6\n1.1.1.1\n6\n1\n\n6\n1\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n6\n1.1.1\nSub-step\n5\n3\n7\n",
      "screen": "== Outline writer ==\nAn outline written as an indented string, read into a tree, and written out as bullets, as numbers, and wrapped to a width.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 4\nwidth (20 to 80)> \nCancelled.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 4\nwidth (20 to 80)> 19\nType a whole number from 20 to 80.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 4\nwidth (20 to 80)> 81\nType a whole number from 20 to 80.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 4\nwidth (20 to 80)> x\nType a whole number from 20 to 80.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 5\ndepth (0 to 3)> \nCancelled.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 5\ndepth (0 to 3)> 4\nType a whole number from 0 to 3.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 5\ndepth (0 to 3)> -1\nType a whole number from 0 to 3.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> \nCancelled.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 3\nType the number of an item, such as 1 or 1.2, or 0.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1.9\nType the number of an item, such as 1 or 1.2, or 0.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1..2\nType the number of an item, such as 1 or 1.2, or 0.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> a\nType the number of an item, such as 1 or 1.2, or 0.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1.1.1.1\nType the number of an item, such as 1 or 1.2, or 0.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1\ntext> \nCancelled.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1\ntext> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nUp to 120 characters.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1.1.1\ntext> Sub-step\nAdded under 1.1.1: Sub-step\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 5\ndepth (0 to 3)> 3\nWith a with block:\nRelease plan\n  Build\n    Compile the interpreter and the transpiler for every platform we ship\n      Sub-step\n    Run the whole test suite\n  Check\n    Read every golden line by line\n    Cross-check each claim a second way, written separately\n  Publish\n    Build the site\n    Deploy\nAfterwards\n  Write the journal\nKeeping the depth by hand, an early return skipping the step back:\nRelease plan\n  Build\n    Compile the interpreter and the transpiler for every platform we ship\n      Sub-step\n    Run the whole test suite\n  Check\n    Read every golden line by line\n    Cross-check each claim a second way, written separately\n  Publish\n    Build the site\n    Deploy\nAfterwards\n  Write the journal\nThe two agree: nothing was cut, so no early return happened.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n40\n4\n20\n5\n1\n5\n3\n6\n1.3\nTell Splice\n6\n0\nRest\n3\n7\n",
      "screen": "== Outline writer ==\nAn outline written as an indented string, read into a tree, and written out as bullets, as numbers, and wrapped to a width.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 1\nThe source string has 347 characters in 14 lines (| marks where each line starts):\n   1 |\n   2 |    Release plan\n   3 |      Build\n   4 |        Compile the interpreter and the transpiler for every platform we ship\n   5 |        Run the whole test suite\n   6 |      Check\n   7 |        Read every golden line by line\n   8 |        Cross-check each claim a second way, written separately\n   9 |      Publish\n  10 |        Build the site\n  11 |        Deploy\n  12 |    Afterwards\n  13 |      Write the journal\n  14 |\nReading it: 2 blank lines at the ends dropped (the empty line right after the opening quotes, and the empty one before the closing quotes), and the 4 spaces every line shares taken off.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 2\n- Release plan\n  * Build\n    + Compile the interpreter and the transpiler for every platform we ship\n    + Run the whole test suite\n  * Check\n    + Read every golden line by line\n    + Cross-check each claim a second way, written separately\n  * Publish\n    + Build the site\n    + Deploy\n- Afterwards\n  * Write the journal\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 3\n1 Release plan\n  1.1 Build\n    1.1.1 Compile the interpreter and the transpiler for every platform we ship\n    1.1.2 Run the whole test suite\n  1.2 Check\n    1.2.1 Read every golden line by line\n    1.2.2 Cross-check each claim a second way, written separately\n  1.3 Publish\n    1.3.1 Build the site\n    1.3.2 Deploy\n2 Afterwards\n  2.1 Write the journal\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 4\nwidth (20 to 80)> 40\nWrapped at 40, each line after the first lined up under its text:\n+----------------------------------------+\n1 Release plan\n  1.1 Build\n    1.1.1 Compile the interpreter and\n          the transpiler for every\n          platform we ship\n    1.1.2 Run the whole test suite\n  1.2 Check\n    1.2.1 Read every golden line by line\n    1.2.2 Cross-check each claim a\n          second way, written separately\n  1.3 Publish\n    1.3.1 Build the site\n    1.3.2 Deploy\n2 Afterwards\n  2.1 Write the journal\n+----------------------------------------+\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 4\nwidth (20 to 80)> 20\nWrapped at 20, each line after the first lined up under its text:\n+--------------------+\n1 Release plan\n  1.1 Build\n    1.1.1 Compile\n          the\n          interpreter   <- past the width\n          and the\n          transpiler\n          for every\n          platform\n          we ship\n    1.1.2 Run the\n          whole test\n          suite\n  1.2 Check\n    1.2.1 Read every\n          golden\n          line by\n          line\n    1.2.2 Cross-check   <- past the width\n          each claim\n          a second\n          way,\n          written\n          separately\n  1.3 Publish\n    1.3.1 Build the\n          site\n    1.3.2 Deploy\n2 Afterwards\n  2.1 Write the\n      journal\n+--------------------+\n2 lines run past the width: words longer than the room left beside their numbers.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 5\ndepth (0 to 3)> 1\nWith a with block:\nRelease plan\n  Build\n    (2 more)\n  Check\n    (2 more)\n  Publish\n    (2 more)\nAfterwards\n  Write the journal\nKeeping the depth by hand, an early return skipping the step back:\nRelease plan\n  Build\n    (2 more)\n    Check\n      (2 more)\n      Publish\n        (2 more)\n      Afterwards\n        Write the journal\n6 of the 9 lines differ: each early return leaves every later line by hand one more step too far in.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 5\ndepth (0 to 3)> 3\nWith a with block:\nRelease plan\n  Build\n    Compile the interpreter and the transpiler for every platform we ship\n    Run the whole test suite\n  Check\n    Read every golden line by line\n    Cross-check each claim a second way, written separately\n  Publish\n    Build the site\n    Deploy\nAfterwards\n  Write the journal\nKeeping the depth by hand, an early return skipping the step back:\nRelease plan\n  Build\n    Compile the interpreter and the transpiler for every platform we ship\n    Run the whole test suite\n  Check\n    Read every golden line by line\n    Cross-check each claim a second way, written separately\n  Publish\n    Build the site\n    Deploy\nAfterwards\n  Write the journal\nThe two agree: nothing was cut, so no early return happened.\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 1.3\ntext> Tell Splice\nAdded under 1.3: Tell Splice\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 6\nunder (an item's number such as 1.2, or 0 for the top)> 0\ntext> Rest\nAdded at the top: Rest\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 3\n1 Release plan\n  1.1 Build\n    1.1.1 Compile the interpreter and the transpiler for every platform we ship\n    1.1.2 Run the whole test suite\n  1.2 Check\n    1.2.1 Read every golden line by line\n    1.2.2 Cross-check each claim a second way, written separately\n  1.3 Publish\n    1.3.1 Build the site\n    1.3.2 Deploy\n    1.3.3 Tell Splice\n2 Afterwards\n  2.1 Write the journal\n3 Rest\n\n1) source  2) bullets  3) numbered  4) wrapped  5) to a depth  6) add an item  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "indented-report-writer",
      "caseId": "144-indented-report-writer",
      "title": "A context manager that isn't about resources"
    },
    {
      "slug": "multiline-help-text",
      "caseId": "146-multiline-help-text",
      "title": "Triple-quoted strings, and the whitespace they capture"
    }
  ],
  "updated": "2026-10-17"
}
