๐Ÿ”ฌ Testing Section grouping

Table of contents

Sections

Pseudo code steps, operating during parse of standard mdast.

  • With a stack initialised with root node (with children removed) ie [root]
  • Iterate over all removed root chilren (in reverse? TBD) as node
    • define section as top of stack
    • if section is root
      • create section with node depth or 0.
      • add node as child of section
      • push section to children of node at the top of stack
      • push section to top of stack
    • else if node is a heading
      • with deeper depth?
        • create section with node depth
        • add node as child of section
        • push section to children of node at the top of stack
        • push section to top of stack
      • else
        • for each section in stack until top of stack has depth equal to or lower (shallower) than node depth.
          • pop stack and call process handler on popped section
        • create section with node depth
        • add node as child of section
        • push section to children of node at the top of stack
        • push section to top of stack
    • else add node to section
    • if no next node
      • for each section in stack until but not including root
        • pop stack and call process handler on popped section
js../parser/sections-v3.js
// This files source is defined in [[testing/section_grouping|๐Ÿ”ฌ Testing Section grouping]]

export const sections = (options) => (...args) => (tree) => {
  try {
  const {processSection} = options;
  const stack = [tree];
  const nodes = tree.children;
  tree.children = [];

  const createSection = (node) => {
    if (!node.position) throw new Error(`Node ${node.type} has no position`)
    if (!node.data?.id) {
      // throw new Error(`Node ${node.type} has no data.id`)
    }

    const section = {
      type: 'section',
      children: [node],
      depth: node.depth || 0,
      position: node.position,
      data: {
        name: node.data?.id,
        hName: 'section',
        hProperties: {
          depth: node.depth || 0,
          id: node.data?.id,
        }
      },
    };
    stack[stack.length-1].children.push(section);
    stack.push(section);
  }

  const shouldPopStack = (n, force) => {
    const s = stack[stack.length-1];
    const notRoot = s.type !== 'root';
    const nShallow = s.depth >= n.depth;
    return notRoot && (force || nShallow);
  }

  const endSection = () => {
    const s = stack.pop();
    if (processSection) {
      try {
        processSection(s);
      } catch(err) {
        throw new Error(`Failed to processSection due to ${err.message}`);
      }
    }
  }

  nodes.map((node, index) => {
    const section = stack[stack.length-1];
    if (section.type === 'root') {
      createSection(node);
    } else if (node.type === 'heading') {
      if (node.depth > section.depth) {
        createSection(node);
      } else {
        while (shouldPopStack(node)) {
          endSection();
        }
        createSection(node);
      }
    } else {
      // some list nodes have no position (toc?)
      if (!section.position) { throw new Error(`Section ${section.type} has no position`)}
      section.children.push(node);
      if (node.position) section.position.end = node.position.end;
    }

    if (!nodes[index+1]) {
      while (shouldPopStack(node, true)) {
          endSection();
        }
    }
  });

  } catch(err) {
    throw new Error(`Failed to group sections due to ${err.message}`);
  }

}

Cells

  • given a set of nodes (generally children of a section)
  • define an empty stack []
  • for each node in nodes
    • if node type is section push to top of stack
    • else if node type is a code
      • if previous cell is a code cell and node is attached
        • push node as child of code cell at top of stack
      • else
        • create new cell with node as first child
        • push cell to top of stack
    • else if stack is empty or top of stack is a section or code cell
      • create new cell with node as first child
      • push cell to top of stack
    • else push node to children of cell at top of stack
js../parser/cells-v3.js
export const cells = (section) => {
  const stack = []
  const nodes = section.children
  section.children = stack

  const needNewCell = () => {
    const cell = stack[stack.length-1]
    return !cell 
    || cell.data?.code 
    || cell.type === 'section' 
    || cell.children?.[0]?.type === 'heading';
  }

  const createCell = (node) => {
    stack.push({
       type: 'cell',
       children: [node],
       position: node.position,
       data: {
         code: node.type === 'code',
         hName: "cell",
         hProperties: {
           class: "cell",
         },
       },
     })
  }

  const addToCell = (node) => {
    const cell = stack[stack.length-1]
    cell.children.push(node)
    if (node.position) cell.position.end = node.position.end
  }

  nodes.map((node, index) => {
    const prev = index ? stack[index-1] : null
    if (node.type === 'section')
      stack.push(node)
    else if (node.type === 'code') {
      if (prev?.data?.code && node.data?.meta?.attached) {
        addToCell(node)
      } else {
        createCell(node)
      }
    } else if (needNewCell()) {
      createCell(node)
    } else {
      addToCell(node)
    }
  })

  return section
}

Potential improvements & optimisations

  • TODO perform cell grouping internal to section node collection, preventing unnecessary additional loops.
  • list nodes (with no position? had assumed only toc) being grouped into prior code cells