Repository: zestedesavoir/zmarkdown
Pull request #322 (2019-04-14 12:52:37)

mdast-util-split-by-heading
@artragis renamed, rebased on top of master, adapted the README and `./dist` added.

I think it's a nice package, looking forward to seeing what you will build with it.

Superseding #315 


Patch:
diff --git a/README.md b/README.md
index 5f47b990..582e77e0 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,10 @@ This project uses [Jest][jest] for testing. It is recommended to install the Jes
 
 ## Packages
 
+* [**mdast-util-split-by-heading**][mdast-util-split-by-heading]
+
+  A MDAST tool to split a markdown tree into list of subtrees representing the chapters. It relies on heading depth.
+
 * [**rebber**][rebber]
 
   transformation of MDAST into `latex` code. This code must be included inside a custom latex to be compiled.
@@ -148,6 +152,7 @@ This project uses [Jest][jest] for testing. It is recommended to install the Jes
 
 [jest]: https://facebook.github.io/jest/
 
+[mdast-util-split-by-heading]: https://github.com/zestedesavoir/zmarkdown/tree/master/packages/mdast-util-split-by-heading#mdast-util-split-by-heading--
 [rebber]: https://github.com/zestedesavoir/zmarkdown/tree/master/packages/rebber#rebber--
 [rebber-plugins]: https://github.com/zestedesavoir/zmarkdown/tree/master/packages/rebber-plugins#rebber-plugins--
 [remark-abbr]: https://github.com/zestedesavoir/zmarkdown/tree/master/packages/remark-abbr#remark-abbr--
diff --git a/packages/mdast-util-split-by-heading/.babelrc b/packages/mdast-util-split-by-heading/.babelrc
new file mode 100644
index 00000000..216f5289
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/.babelrc
@@ -0,0 +1,9 @@
+{
+  "presets": ["@babel/preset-env"],
+  "plugins": [],
+  "env": {
+    "development": {
+      "sourceMaps": "inline"
+    }
+  }
+}
diff --git a/packages/mdast-util-split-by-heading/.eslintignore b/packages/mdast-util-split-by-heading/.eslintignore
new file mode 100644
index 00000000..7377bc57
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/.eslintignore
@@ -0,0 +1,4 @@
+coverage
+public/*
+packages/*/dist/*
+dist/
diff --git a/packages/mdast-util-split-by-heading/.eslintrc.json b/packages/mdast-util-split-by-heading/.eslintrc.json
new file mode 100644
index 00000000..d45698d5
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/.eslintrc.json
@@ -0,0 +1,186 @@
+{
+  "parserOptions": {
+    "ecmaVersion": 6,
+    "sourceType": "module"
+  },
+
+  "env": {
+    "es6": true,
+    "node": true,
+    "mocha": true
+  },
+
+  "plugins": [],
+
+  "globals": {
+    "document": false,
+    "navigator": false,
+    "window": false,
+    "expect": true
+  },
+
+  "rules": {
+    "accessor-pairs": 2,
+    "array-bracket-spacing": [1, "never"],
+    "arrow-spacing": [2, {"before": true, "after": true}],
+    "block-spacing": [2, "always"],
+    "brace-style": [2, "1tbs", {"allowSingleLine": true}],
+    "camelcase": [2, {"properties": "never"}],
+    "comma-dangle": [1, "always-multiline"],
+    "comma-spacing": [2, {"before": false, "after": true}],
+    "comma-style": [2, "last"],
+    "computed-property-spacing": [2, "never"],
+    "constructor-super": 2,
+    "curly": [2, "multi-line"],
+    "dot-location": [2, "property"],
+    "eol-last": [2, "always"],
+    "eqeqeq": [2, "allow-null"],
+    "func-call-spacing": [2, "never"],
+    "callback-return": [1, ["callback", "cb", "done"]],
+    "handle-callback-err": [2, "^(err|error)$"],
+    "indent": [2, 2, {
+      "SwitchCase": 1,
+      "VariableDeclarator": 1,
+      "outerIIFEBody": 1,
+      "FunctionDeclaration": {
+        "parameters": 1,
+        "body": 1
+      },
+      "FunctionExpression": {
+        "parameters": 1,
+        "body": 1
+      }
+    }],
+    "key-spacing": [2, {"beforeColon": false, "afterColon": true}],
+    "keyword-spacing": [2, {"before": true, "after": true}],
+    "linebreak-style": [2, "unix"],
+    "max-len": ["error", {
+      "code": 100,
+      "ignoreRegExpLiterals": true,
+      "ignorePattern": "^\\s*const\\s.+=\\s*require\\s*\\("
+    }],
+    "new-cap": [2, {"newIsCap": true, "capIsNew": false}],
+    "new-parens": 2,
+    "newline-per-chained-call": [2, {"ignoreChainWithDepth": 4}],
+    "no-array-constructor": 2,
+    "no-caller": 2,
+    "no-class-assign": 2,
+    "no-cond-assign": 2,
+    "no-console": [1, {"allow": ["error"]}],
+    "no-const-assign": 2,
+    "no-constant-condition": [2, {"checkLoops": false}],
+    "no-control-regex": 2,
+    "no-debugger": 2,
+    "no-delete-var": 2,
+    "no-dupe-args": 2,
+    "no-dupe-class-members": 2,
+    "no-dupe-keys": 2,
+    "no-duplicate-case": 2,
+    "no-duplicate-imports": 2,
+    "no-empty-character-class": 2,
+    "no-empty-pattern": 2,
+    "no-eval": 2,
+    "no-ex-assign": 2,
+    "no-extend-native": 2,
+    "no-extra-bind": 2,
+    "no-extra-boolean-cast": 2,
+    "no-extra-parens": [2, "functions"],
+    "no-fallthrough": 2,
+    "no-floating-decimal": 2,
+    "no-func-assign": 2,
+    "no-global-assign": 2,
+    "no-implied-eval": 2,
+    "no-inner-declarations": [2, "functions"],
+    "no-invalid-regexp": 2,
+    "no-irregular-whitespace": 2,
+    "no-iterator": 2,
+    "no-label-var": 2,
+    "no-labels": [2, {"allowLoop": false, "allowSwitch": false}],
+    "no-lone-blocks": 2,
+    "no-lonely-if": 2,
+    "no-mixed-operators": [2, {
+      "groups": [
+        ["+", "-", "*", "/", "%", "**"],
+        ["&", "|", "^", "~", "<<", ">>", ">>>"],
+        ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
+        ["&&", "||"],
+        ["in", "instanceof"]
+      ],
+      "allowSamePrecedence": false
+    }],
+    "no-mixed-spaces-and-tabs": 2,
+    "no-multi-spaces": 2,
+    "no-multi-str": 2,
+    "no-multiple-empty-lines": [2, {"max": 2}],
+    "no-native-reassign": 2,
+    "no-negated-in-lhs": 2,
+    "no-nested-ternary": 2,
+    "no-new": 2,
+    "no-new-func": 2,
+    "no-new-object": 2,
+    "no-new-require": 2,
+    "no-new-symbol": 2,
+    "no-new-wrappers": 2,
+    "no-obj-calls": 2,
+    "no-octal": 2,
+    "no-octal-escape": 2,
+    "no-path-concat": 2,
+    "no-proto": 2,
+    "no-redeclare": 2,
+    "no-regex-spaces": 2,
+    "no-return-assign": [2, "except-parens"],
+    "no-self-assign": 2,
+    "no-self-compare": 2,
+    "no-sequences": 2,
+    "no-shadow-restricted-names": 2,
+    "no-sparse-arrays": 2,
+    "no-tabs": 2,
+    "no-template-curly-in-string": 2,
+    "no-this-before-super": 2,
+    "no-throw-literal": 2,
+    "no-trailing-spaces": 2,
+    "no-undef": 2,
+    "no-undef-init": 2,
+    "no-unexpected-multiline": 2,
+    "no-unmodified-loop-condition": 2,
+    "no-unneeded-ternary": [2, {"defaultAssignment": false}],
+    "no-unreachable": 2,
+    "no-unsafe-finally": 2,
+    "no-unsafe-negation": 2,
+    "no-unused-vars": [2, {"vars": "all", "args": "none"}],
+    "no-useless-call": 2,
+    "no-useless-computed-key": 2,
+    "no-useless-constructor": 2,
+    "no-useless-escape": 2,
+    "no-useless-rename": 2,
+    "no-var": 2,
+    "no-whitespace-before-property": 2,
+    "no-with": 2,
+    "object-curly-spacing": [1, "never"],
+    "object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}],
+    "one-var": [2, {"initialized": "never"}],
+    "operator-linebreak": [2, "after", {"overrides": {"?": "before", ":": "before"}}],
+    "padded-blocks": [0, "never"],
+    "prefer-template": 2,
+    "prefer-const": [2, {"destructuring": "any", "ignoreReadBeforeAssign": true}],
+    "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
+    "rest-spread-spacing": [2, "never"],
+    "semi": [2, "never"],
+    "semi-spacing": [2, {"before": false, "after": true}],
+    "space-before-blocks": [2, "always"],
+    "space-before-function-paren": [2, "always"],
+    "space-in-parens": [2, "never"],
+    "space-infix-ops": 2,
+    "space-unary-ops": [2, {"words": true, "nonwords": false}],
+    "spaced-comment": [2, "always",
+      {"line": {"markers": ["*package", "!", ","]}, "block": {"balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"]}}
+    ],
+    "template-curly-spacing": [2, "never"],
+    "unicode-bom": [2, "never"],
+    "use-isnan": 2,
+    "valid-typeof": 2,
+    "wrap-iife": [2, "any", {"functionPrototypeMethods": true}],
+    "yield-star-spacing": [2, "both"],
+    "yoda": [2, "never"]
+  }
+}
diff --git a/packages/mdast-util-split-by-heading/LICENSE-MIT b/packages/mdast-util-split-by-heading/LICENSE-MIT
new file mode 100644
index 00000000..ba94038c
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) Zeste de Savoir (https://zestedesavoir.com)
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/mdast-util-split-by-heading/README.md b/packages/mdast-util-split-by-heading/README.md
new file mode 100644
index 00000000..fba3a18f
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/README.md
@@ -0,0 +1,468 @@
+# mdast-util-split-by-heading [![Build Status][build-badge]][build-status] [![Coverage Status][coverage-badge]][coverage-status]
+
+
+**mdast-util-split-by-heading** splits a markdown AST into several markdown ASTs based on their headings.
+
+It is useful when you want to split a document with many headings into several documents, for instance one by chapter.
+
+## Installation
+
+[npm][]:
+
+```bash
+npm install mdast-util-split-by-heading
+```
+
+## Usage
+
+```javascript
+const unified = require('unified')
+const parse = require('remark-parse')
+const split = require('mdast-util-split-by-heading')
+
+var tree = unified()
+  .use(parse)
+  .parse('# part\n\n## chapter \n\n Hello world \n\n # part *2*')
+
+console.log(split(tree))
+```
+
+## API
+
+### `split(node, options = { splitDepth: 1 })`
+
+Splits a MDAST tree into separate trees by [heading depth](https://github.com/syntax-tree/mdast#heading).
+
+#### `options.splitDepth = 1`
+
+An integer greater or equal to 1 determining the heading depth you want to match when splitting.
+
+## Examples:
+
+```js
+import dedent from 'dedent'
+import unified from 'unified'
+import reParse from 'remark-parse'
+import split from 'mdast-util-split-by-heading'
+
+const doSplit = (text, options) => {
+  const { splitDepth = 1 } = options
+  return split(
+    unified().use(reParse).parse(text),
+    { splitDepth: splitDepth }
+  )
+}
+
+const text = dedent `
+  a global introduction
+
+  # hello
+
+  a paragraph
+
+  > a quote to *ensure this is parsed*
+
+  ## a sub title
+
+  other paragraph
+
+  # conclusion title
+
+  paragraph
+  `
+
+doSplit(text)
+/*
+{
+  "introduction": {
+    "type": "root",
+    "children": [
+      {
+        "type": "paragraph",
+        "children": [
+          {
+            "type": "text",
+            "value": "a global introduction",
+            "position": {
+              "start": {
+                "line": 1,
+                "column": 1,
+                "offset": 0
+              },
+              "end": {
+                "line": 1,
+                "column": 22,
+                "offset": 21
+              },
+              "indent": []
+            }
+          }
+        ],
+        "position": {
+          "start": {
+            "line": 1,
+            "column": 1,
+            "offset": 0
+          },
+          "end": {
+            "line": 1,
+            "column": 22,
+            "offset": 21
+          },
+          "indent": []
+        }
+      }
+    ]
+  },
+  "trees": [
+    {
+      "title": {
+        "type": "root",
+        "children": {
+          "type": "heading",
+          "depth": 1,
+          "children": [
+            {
+              "type": "text",
+              "value": "hello",
+              "position": {
+                "start": {
+                  "line": 3,
+                  "column": 3,
+                  "offset": 25
+                },
+                "end": {
+                  "line": 3,
+                  "column": 8,
+                  "offset": 30
+                },
+                "indent": []
+              }
+            }
+          ],
+          "position": {
+            "start": {
+              "line": 3,
+              "column": 1,
+              "offset": 23
+            },
+            "end": {
+              "line": 3,
+              "column": 8,
+              "offset": 30
+            },
+            "indent": []
+          }
+        }
+      },
+      "children": {
+        "type": "root",
+        "children": [
+          {
+            "type": "paragraph",
+            "children": [
+              {
+                "type": "text",
+                "value": "a paragraph",
+                "position": {
+                  "start": {
+                    "line": 5,
+                    "column": 1,
+                    "offset": 32
+                  },
+                  "end": {
+                    "line": 5,
+                    "column": 12,
+                    "offset": 43
+                  },
+                  "indent": []
+                }
+              }
+            ],
+            "position": {
+              "start": {
+                "line": 5,
+                "column": 1,
+                "offset": 32
+              },
+              "end": {
+                "line": 5,
+                "column": 12,
+                "offset": 43
+              },
+              "indent": []
+            }
+          },
+          {
+            "type": "blockquote",
+            "children": [
+              {
+                "type": "paragraph",
+                "children": [
+                  {
+                    "type": "text",
+                    "value": "a quote to ",
+                    "position": {
+                      "start": {
+                        "line": 7,
+                        "column": 3,
+                        "offset": 47
+                      },
+                      "end": {
+                        "line": 7,
+                        "column": 14,
+                        "offset": 58
+                      },
+                      "indent": []
+                    }
+                  },
+                  {
+                    "type": "emphasis",
+                    "children": [
+                      {
+                        "type": "text",
+                        "value": "ensure this is parsed",
+                        "position": {
+                          "start": {
+                            "line": 7,
+                            "column": 15,
+                            "offset": 59
+                          },
+                          "end": {
+                            "line": 7,
+                            "column": 36,
+                            "offset": 80
+                          },
+                          "indent": []
+                        }
+                      }
+                    ],
+                    "position": {
+                      "start": {
+                        "line": 7,
+                        "column": 14,
+                        "offset": 58
+                      },
+                      "end": {
+                        "line": 7,
+                        "column": 37,
+                        "offset": 81
+                      },
+                      "indent": []
+                    }
+                  }
+                ],
+                "position": {
+                  "start": {
+                    "line": 7,
+                    "column": 3,
+                    "offset": 47
+                  },
+                  "end": {
+                    "line": 7,
+                    "column": 37,
+                    "offset": 81
+                  },
+                  "indent": []
+                }
+              }
+            ],
+            "position": {
+              "start": {
+                "line": 7,
+                "column": 1,
+                "offset": 45
+              },
+              "end": {
+                "line": 7,
+                "column": 37,
+                "offset": 81
+              },
+              "indent": []
+            }
+          },
+          {
+            "type": "heading",
+            "depth": 2,
+            "children": [
+              {
+                "type": "text",
+                "value": "a sub title",
+                "position": {
+                  "start": {
+                    "line": 9,
+                    "column": 4,
+                    "offset": 86
+                  },
+                  "end": {
+                    "line": 9,
+                    "column": 15,
+                    "offset": 97
+                  },
+                  "indent": []
+                }
+              }
+            ],
+            "position": {
+              "start": {
+                "line": 9,
+                "column": 1,
+                "offset": 83
+              },
+              "end": {
+                "line": 9,
+                "column": 15,
+                "offset": 97
+              },
+              "indent": []
+            }
+          },
+          {
+            "type": "paragraph",
+            "children": [
+              {
+                "type": "text",
+                "value": "other paragraph",
+                "position": {
+                  "start": {
+                    "line": 11,
+                    "column": 1,
+                    "offset": 99
+                  },
+                  "end": {
+                    "line": 11,
+                    "column": 16,
+                    "offset": 114
+                  },
+                  "indent": []
+                }
+              }
+            ],
+            "position": {
+              "start": {
+                "line": 11,
+                "column": 1,
+                "offset": 99
+              },
+              "end": {
+                "line": 11,
+                "column": 16,
+                "offset": 114
+              },
+              "indent": []
+            }
+          }
+        ]
+      }
+    },
+    {
+      "title": {
+        "type": "root",
+        "children": {
+          "type": "heading",
+          "depth": 1,
+          "children": [
+            {
+              "type": "text",
+              "value": "conclusion title",
+              "position": {
+                "start": {
+                  "line": 13,
+                  "column": 3,
+                  "offset": 118
+                },
+                "end": {
+                  "line": 13,
+                  "column": 19,
+                  "offset": 134
+                },
+                "indent": []
+              }
+            }
+          ],
+          "position": {
+            "start": {
+              "line": 13,
+              "column": 1,
+              "offset": 116
+            },
+            "end": {
+              "line": 13,
+              "column": 19,
+              "offset": 134
+            },
+            "indent": []
+          }
+        }
+      },
+      "children": {
+        "type": "root",
+        "children": [
+          {
+            "type": "paragraph",
+            "children": [
+              {
+                "type": "text",
+                "value": "paragraph",
+                "position": {
+                  "start": {
+                    "line": 15,
+                    "column": 1,
+                    "offset": 136
+                  },
+                  "end": {
+                    "line": 15,
+                    "column": 10,
+                    "offset": 145
+                  },
+                  "indent": []
+                }
+              }
+            ],
+            "position": {
+              "start": {
+                "line": 15,
+                "column": 1,
+                "offset": 136
+              },
+              "end": {
+                "line": 15,
+                "column": 10,
+                "offset": 145
+              },
+              "indent": []
+            }
+          }
+        ]
+      }
+    }
+  ]
+}
+*/
+```
+
+## License
+
+[MIT][license] © [Zeste de Savoir][zds]
+
+<!-- Definitions -->
+
+[build-badge]: https://img.shields.io/travis/zestedesavoir/zmarkdown.svg
+
+[build-status]: https://travis-ci.org/zestedesavoir/zmarkdown
+
+[coverage-badge]: https://img.shields.io/coveralls/zestedesavoir/zmarkdown.svg
+
+[coverage-status]: https://coveralls.io/github/zestedesavoir/zmarkdown
+
+[license]: https://github.com/zestedesavoir/zmarkdown/blob/master/packages/rebber/LICENSE-MIT
+
+[rebber-plugins]: https://github.com/zestedesavoir/zmarkdown/blob/master/packages/rebber-plugins
+
+[zds]: https://zestedesavoir.com
+
+[npm]: https://www.npmjs.com/package/rebber
+
+[mdast]: https://github.com/syntax-tree/mdast/blob/master/readme.md
+
+[remark]: https://github.com/remarkjs/remark
diff --git a/packages/mdast-util-split-by-heading/dist/index.js b/packages/mdast-util-split-by-heading/dist/index.js
new file mode 100644
index 00000000..78c05337
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/dist/index.js
@@ -0,0 +1,75 @@
+"use strict";
+
+var _unistUtilVisit = _interopRequireDefault(require("unist-util-visit"));
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
+
+function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
+
+module.exports = splitAtDepth;
+
+function splitAtDepth(tree, _ref) {
+  var _ref$splitDepth = _ref.splitDepth,
+      splitDepth = _ref$splitDepth === void 0 ? 1 : _ref$splitDepth;
+  var splitter = new Splitter(splitDepth);
+  (0, _unistUtilVisit["default"])(tree, null, function (node, index, parent) {
+    return splitter.visit(node, index, parent);
+  });
+  return {
+    introduction: splitter.introduction,
+    trees: splitter.subTrees
+  };
+}
+
+function newRootTree() {
+  var children = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
+  return {
+    type: 'root',
+    children: children
+  };
+}
+
+var Splitter =
+/*#__PURE__*/
+function () {
+  function Splitter() {
+    var depth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
+
+    _classCallCheck(this, Splitter);
+
+    this.lastIndex = -1;
+    this.subTrees = [];
+    this.depth = depth;
+    this.introduction = newRootTree();
+  }
+
+  _createClass(Splitter, [{
+    key: "visit",
+    value: function visit(node, index, parent) {
+      if (!parent) {
+        // we are at the root
+        return;
+      }
+
+      if (node.type === 'heading' && node.depth === this.depth) {
+        this.lastIndex = index;
+        var subtree = {
+          title: newRootTree(node),
+          children: newRootTree()
+        };
+        this.subTrees.push(subtree);
+      } else if (parent.type === 'root' && this.lastIndex === -1) {
+        this.introduction.children.push(node);
+        return;
+      } else if (parent.type === 'root') {
+        this.subTrees[this.subTrees.length - 1].children.children.push(node);
+      }
+    }
+  }]);
+
+  return Splitter;
+}();
\ No newline at end of file
diff --git a/packages/mdast-util-split-by-heading/package.json b/packages/mdast-util-split-by-heading/package.json
new file mode 100644
index 00000000..0e107b20
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "mdast-util-split-by-heading",
+  "version": "0.0.1",
+  "description": "Split MDAST into subtrees relying on the header hierarchy.",
+  "repository": "https://github.com/zestedesavoir/zmarkdown/tree/master/packages/mdast-util-split-by-heading",
+  "author": "François (artragis) Dambrine  <perso@francoisdambrine.me>",
+  "contributors": [],
+  "scripts": {
+    "pretest": "eslint .",
+    "prepare": "del-cli dist && cross-env BABEL_ENV=production babel src --out-dir dist",
+    "test": "jest",
+    "coverage": "jest --coverage"
+  },
+  "main": "dist/index.js",
+  "files": [
+    "LICENSE-MIT",
+    "dist",
+    "src",
+    "README.md"
+  ],
+  "engines": {
+    "node": ">=6"
+  },
+  "keywords": [
+    "mdast"
+  ],
+  "license": "MIT",
+  "dependencies": {
+    "unist-util-visit": "^1.3.1"
+  }
+}
diff --git a/packages/mdast-util-split-by-heading/src/index.js b/packages/mdast-util-split-by-heading/src/index.js
new file mode 100644
index 00000000..0dd85ef8
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/src/index.js
@@ -0,0 +1,51 @@
+import visit from 'unist-util-visit'
+
+module.exports = splitAtDepth
+
+function splitAtDepth (tree, {splitDepth = 1}) {
+  const splitter = new Splitter(splitDepth)
+
+  visit(tree, null, (node, index, parent) => splitter.visit(node, index, parent))
+
+  return {
+    introduction: splitter.introduction,
+    trees: splitter.subTrees,
+  }
+}
+
+function newRootTree (children = []) {
+  return {
+    type: 'root',
+    children,
+  }
+}
+
+class Splitter {
+  constructor (depth = 1) {
+    this.lastIndex = -1
+    this.subTrees = []
+    this.depth = depth
+    this.introduction = newRootTree()
+  }
+
+  visit (node, index, parent) {
+    if (!parent) {
+      // we are at the root
+      return
+    }
+
+    if (node.type === 'heading' && node.depth === this.depth) {
+      this.lastIndex = index
+      const subtree = {
+        title: newRootTree(node),
+        children: newRootTree(),
+      }
+      this.subTrees.push(subtree)
+    } else if (parent.type === 'root' && this.lastIndex === -1) {
+      this.introduction.children.push(node)
+      return
+    } else if (parent.type === 'root') {
+      this.subTrees[this.subTrees.length - 1].children.children.push(node)
+    }
+  }
+}


Test patch:
diff --git a/packages/mdast-util-split-by-heading/__tests__/index.js b/packages/mdast-util-split-by-heading/__tests__/index.js
new file mode 100644
index 00000000..ec9a4851
--- /dev/null
+++ b/packages/mdast-util-split-by-heading/__tests__/index.js
@@ -0,0 +1,164 @@
+import dedent from 'dedent'
+import unified from 'unified'
+import reParse from 'remark-parse'
+import split from '../src'
+
+const doSplit = (text, {splitDepth = 1,
+  introductionAsProperty = true, conclusionAsProperty = false}) => {
+  return split(unified().use(reParse).parse(text), {
+    splitDepth: splitDepth,
+    conclusionAsProperty: conclusionAsProperty})
+}
+
+const text = dedent `
+  a global introduction
+
+  # hello
+
+  a paragraph
+
+  > a quote to *ensure this is parsed*
+
+  ## a sub title
+
+  other paragraph
+
+  # conclusion title
+
+  paragraph
+  `
+
+test('default parameter with canonical text', () => {
+  const result = doSplit(text, {})
+  expect(result.introduction).toMatchObject({
+    type: 'root',
+    children: [
+      {
+        type: 'paragraph',
+        children: [
+          {
+            type: 'text',
+            value: 'a global introduction',
+          },
+        ],
+      },
+    ],
+  })
+  expect(result.trees.length).toBe(2)
+  expect(result.conclusion).toBeFalsy()
+  expect(result.trees[0].children).toMatchObject({
+    type: 'root',
+    children: [
+      {
+        type: 'paragraph',
+        children: [
+          {
+            type: 'text',
+            value: 'a paragraph',
+          },
+        ],
+      },
+      {
+        type: 'blockquote',
+        children: [
+          {
+            type: 'paragraph',
+            children: [
+              {
+                type: 'text',
+                value: 'a quote to ',
+              },
+              {
+                type: 'emphasis',
+                children: [
+                  {
+                    type: 'text',
+                    value: 'ensure this is parsed',
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+      {
+        type: 'heading',
+        depth: 2,
+        children: [
+          {
+            type: 'text',
+            value: 'a sub title',
+          },
+        ],
+      },
+      {
+        type: 'paragraph',
+        children: [
+          {
+            type: 'text',
+            value: 'other paragraph',
+          },
+        ],
+      },
+    ],
+  })
+  expect(result.trees[1].children).toMatchObject({
+    type: 'root',
+    children: [
+      {
+        type: 'paragraph',
+        children: [
+          {
+            type: 'text',
+            value: 'paragraph',
+          },
+        ],
+      },
+    ],
+  })
+})
+
+test('no heading', () => {
+  const headingStripped = text.replace(/#/g, '')
+  const result = doSplit(headingStripped, {})
+  expect(result.trees).toHaveLength(0)
+  expect(result.introduction.type).toBe('root')
+})
+
+test('split level 2 titles', () => {
+  const result = doSplit(text, {splitDepth: 2})
+  expect(result.trees.length).toBe(1)
+  expect(result.trees[0].children).toMatchObject({
+    type: 'root',
+    children: [
+      {
+        type: 'paragraph',
+        children: [
+          {
+            type: 'text',
+            value: 'other paragraph',
+          },
+        ],
+      },
+      {
+        type: 'heading',
+        depth: 1,
+        children: [
+          {
+            type: 'text',
+            value: 'conclusion title',
+          },
+        ],
+      },
+      {
+        type: 'paragraph',
+        children: [
+          {
+            type: 'text',
+            value: 'paragraph',
+          },
+        ],
+      },
+    ],
+  })
+})