Closed Norlandz closed 1 year ago
Hey, thanks for bringing this up. Accomplishing what you are describing is not currently possible as tocbot doesn't handle heading margins like that right now. However, using some custom custom CSS you may be able to accomplish this.
something like:
.node-name--H3 ~ .toc-list .node-name--H5 { margin-left: 10px }
Hopefully that is helpful. I'd also be open to a contribution to add this feature if you are able to implement it.
Spent couple days to implement it. Not the perfect. You may (or may not) need to look through to fix some of it.
Added additional features (categorized into 3, see comments inside), (so it turns out become much larger than I thought...)
To use the script. Paste below js code into tocbot.js
at proper location, this should replaces some of the old codes.
(The old feature will lost after replacing the old code.)
Read the comments inside for much clearer explanation.
output image for tocbot
(Preface repeated from the comment inside the js code::)
js code (tocbot.js nestHeadingsArray()
)
'|")rel(?(\+|\-)\d+)\k $/g)]; if (arr_matcherArr.length === 0) { // a absolute value let headingLv = parseInt(css_HeadingLv); if (headingLv > 0) { cssInfo_headingLevel.value_absolute = headingLv; cssInfo_headingLevel.det_IsHeadingLevelRelative = false; cssInfo_headingLevel.det_ManualSetInCssFile = true; } else { console.error(css_HeadingLv); console.error(elt); throw new Error('invalid value for css var ' + tocbot_hihesnode_headinglv + ' -- need to be > 0'); } } else { // a relative value let matcher_First = arr_matcherArr[0]; cssInfo_headingLevel.det_IsHeadingLevelRelative = true; cssInfo_headingLevel.value_relative = parseInt(matcher_First.groups.relativeHeadingLvValue); cssInfo_headingLevel.det_ManualSetInCssFile = true; } } else { let tagName = elt.tagName; // @eg: 'H4' / 'span' let arr_matcherArr = [...tagName.matchAll(/^H(?\d)$/gi)]; // convert heading lv from
..., @eg: H4 to 4, when css var is not set if (arr_matcherArr.length !== 0) { let matcher_First = arr_matcherArr[0]; cssInfo_headingLevel.value_absolute = parseInt(matcher_First.groups.headingLv); cssInfo_headingLevel.det_IsHeadingLevelRelative = false; cssInfo_headingLevel.det_ManualSetInCssFile = false; } // default action if css var is not set & not a
... heading element else { // heading level is not set -- use default treatment (`hihesNode_parent.headingLevel +1`) cssInfo_headingLevel.det_IsHeadingLevelRelative = true; cssInfo_headingLevel.value_relative = 1; cssInfo_headingLevel.det_ManualSetInCssFile = false; } } return cssInfo_headingLevel; } //
// // @functionality:{ // provided with a linear array (NodeList) of heading elements (more percisely, an array of elt_PickedToPresentInTocbot, which may contain elements that are not_a_heading) // converts the linear array into a hierarchy tree with nodes -- base on the heading level & order of the elements // returns that root node // } // --- // the whole @key code of this function is inside // `for (let elt_Heading_curr of Array.from(headingsArray)) {` // --- // this is a DFS (depth first search) linear loop (through the array) // (though, not fully linear -- cuz ) // @logic,rule:{ // it climbs up/down to find the appropriate node_parent (a_heading) to append the node_curr (a_heading / not_a_heading) // appropriate means:_ // 1. // deeper level headings must be under shallower level headings @eg: h2 must under h1 // 2. // the heading level of each heading direclty below,, must be exactly 1 greater than,, the level of heading directly above -- @eg: h4 must be under h3 // if @ie,eg: there is a missing heading h3 between h2 & h4 // then a/many node_dummyPadding is created to fill that gap [[-- this is the key to let the headngs indented to correct position -- the issue I post in Github]] // 3. // when receving an element that is NotAHeading but PickedToPresentInTocbot // 3.1 // the heading level of elt_NotAHeading should be `nodeHierarchy_parent.levelHierarchy +1` // ~/updated no_longer_apply/ [[the design of `fixed at some heading level` is abandont (for now)]] // ~/updated no_longer_apply/ [[the design of `dynamic controlable relative heading level -- @ie: nodeHierarchy_parent.levelHierarchy +levelHierarchyInterval_RelToParent` is not implemented (for now) -- could be implemented by adding more node_dummyPadding when needed I_think //think-design_self_better]] // 3.2 // no node should be under the node_NotAHeading // } // @cases:{ // when comparing `the heading level of the node_curr` vs `the heading level of the node_prev` // 1. if ==, -> find node_parent of node_prev, append node_curr into node_parent.child_array // 1. if > && just 1 greater, -> find node_prev, append node_curr into node_prev.child_array // 1. if > && more than 1 greater, -> find node_prev, append a node_dummyPadding into node_prev.child_array, then append node_curr into that node_dummyPadding.child_array; (if needed create more & deeper node_dummyPadding, until appropriate) // 1. if <, -> find node_parent / node_grandparent / node_grandgrandparent ... of node_prev, until appropriate, then append node_curr ... // 1. 1st item in loop case // 1. root case // 1. elt_NotAHeading case // 1. ... // } // // // @version-modify: this correspond to the prev ori tocbot code [`nestHeadingsArray(headingsArray)` + `addNode(node, nest)` + `getHeadingObject(heading)`] // nestHeadingsArray_Nt(headingsArray) function convert_LinearArrayWithHeadings_to_HihesNode(headingsArray) { // root node let hihesNode_root = HihesNode.initBuilder(null, null).build_hihesNode_root(); // current looping element `elt_Heading_curr` will be converted into this `hihesNode_Heading_currDfs` (/ `node_curr`), // and will be appended to an appropriate `node_parent` // by searching through the nodes&linkage stored in `hihesNode_Heading_prevDfs` let hihesNode_Heading_currDfs; let hihesNode_Heading_prevDfs = hihesNode_root; // `hihesNode_Heading_prevDfs = hihesNode_Heading_currDfs;` at the end of for loop // #>- linear loop Dfs // [[seem no need recursion, linear loop works... //think]] // [[the prev origial nestHeadingsArray() use `reduce()` //think]] for (let elt_HeadingOrNot_curr of Array.from(headingsArray)) { // headingsArray is a NodeList // #>>> get heading info let cssInfo_headingLevel_CurrELt = getHeadingLevelInHihesNode(elt_HeadingOrNot_curr); let headingLevel_CurrElt = cssInfo_headingLevel_CurrELt.value_absolute; let det_IsHeadingLevelRelative_currElt = cssInfo_headingLevel_CurrELt.det_IsHeadingLevelRelative; let headingLevel_rel_currElt = cssInfo_headingLevel_CurrELt.value_relative; let cssInfo_IsHeading_CurrELt = detIsHihesHeadingNode(elt_HeadingOrNot_curr); let det_IsHihesHeadingNode_currELt = cssInfo_IsHeading_CurrELt.value; // #>>>>< calculate relative heading level & get hihesNode_ClosestRelativable_parent let hihesNode_ClosestRelativable_parent; hihesNode_ClosestRelativable_parent = hihesNode_Heading_prevDfs; if (hihesNode_Heading_prevDfs.det_IsHeadingLevelRelativable === false) { hihesNode_ClosestRelativable_parent = hihesNode_Heading_prevDfs.hihesNode_ClosestRelativable_parent; // ;no_longer_apply; @flow-direction: stepping upward // ;no_longer_apply; this is the only time which `hihesNode_Heading_prevDfs_currHierarchyDownwardChild` actually steps up } if (det_IsHeadingLevelRelative_currElt === true) { // need put _ same +a_relative_value _ layer headingLevel_CurrElt = hihesNode_ClosestRelativable_parent.headingLevel + headingLevel_rel_currElt; if (headingLevel_CurrElt <= 0) { headingLevel_CurrElt = 1; } } // #>>> put in same layer if (headingLevel_CurrElt === hihesNode_Heading_prevDfs.headingLevel) { //rt: // 1. if ==, -> find node_parent of node_prev, append node_curr into node_parent.child_array let hihesNode_Heading_currDfs_builder = HihesNode.initBuilder(elt_HeadingOrNot_curr, hihesNode_Heading_prevDfs.hihesNode_parent); hihesNode_Heading_currDfs = hihesNode_Heading_currDfs_builder.build_hihesNode_HeadingOrNot_RelativeOrNot(det_IsHihesHeadingNode_currELt, det_IsHeadingLevelRelative_currElt, headingLevel_rel_currElt, hihesNode_ClosestRelativable_parent); } // #>>> put in deeper layer else if (headingLevel_CurrElt > hihesNode_Heading_prevDfs.headingLevel) { let i = 0; let hihesNode_Heading_prevDfs_currHierarchyDownwardChild = hihesNode_Heading_prevDfs; // @duplicated-check: already done in `if (det_HeadingLevelIsRelativeToPrecedingHeading_CurrElt === true)`, but fine if (hihesNode_Heading_prevDfs.det_IsHihesHeadingNode === false) { hihesNode_Heading_prevDfs_currHierarchyDownwardChild = hihesNode_Heading_prevDfs.hihesNode_parent; //@flow-direction: stepping upward // ;no_longer_apply; this is the only time which `hihesNode_Heading_prevDfs_currHierarchyDownwardChild` actually steps up } //rt: // 1. if > && just 1 greater, -> find node_prev, append node_curr into node_prev.child_array //rt: // 1. if > && more than 1 greater, -> find node_prev, append a node_dummyPadding into node_prev.child_array, then append node_curr into that node_dummyPadding.child_array; (if needed create more & deeper node_dummyPadding, until appropriate) while (headingLevel_CurrElt > hihesNode_Heading_prevDfs_currHierarchyDownwardChild.headingLevel) { // @duplicated-check: already done in `else if (headingLevel_CurrElt > hihesNode_Heading_prevDfs.levelHierarchy) {`, but fine i++; // path[direct +1 below node_prevDfs]-normal(90%) // path[dummy]-normal(70%) // path[prevDfsNode is not a Heading]-normal(25%) & ... -> path[dummy]-normal(70%) / ... // just means the even below path `not a heading` will re-loop & go through this path below let levelHeading_LvOfPrevDfsNodePlusOne = hihesNode_Heading_prevDfs_currHierarchyDownwardChild.headingLevel + 1; // path[direct +1 below node_prevDfs]-normal(90%) -- if only loop once // path[dummy]-normal(70%) -- if only loop more than once if (headingLevel_CurrElt === levelHeading_LvOfPrevDfsNodePlusOne) { //@main: //repeat-from[case direct +1 below node_prevDfs]-explain: 1. convert elt_Heading_curr to a new NodeHierarchy instance `node_curr`(/`hihesNode_Heading_currDfs`), //repeat-from[case direct +1 below node_prevDfs]-explain: 2. append `node_curr` into `node_parent.child_array` (`hihesNode_Heading_prevDfs_currHierarchyDownwardChild.arr_hihesNode_child`) // this is automatically done inside the constructor let hihesNode_Heading_currDfs_builder = HihesNode.initBuilder(elt_HeadingOrNot_curr, hihesNode_Heading_prevDfs_currHierarchyDownwardChild); hihesNode_Heading_currDfs = hihesNode_Heading_currDfs_builder.build_hihesNode_HeadingOrNot_RelativeOrNot(det_IsHihesHeadingNode_currELt, det_IsHeadingLevelRelative_currElt, headingLevel_rel_currElt, hihesNode_ClosestRelativable_parent); break; //@flow-direction: } // path[dummy]-normal(70%) add dummmy padding node, and re-loop into check again else { let hihesNode_Heading_currDfs_builder = HihesNode.initBuilder(elt_HeadingOrNot_curr, hihesNode_Heading_prevDfs_currHierarchyDownwardChild); hihesNode_Heading_currDfs = hihesNode_Heading_currDfs_builder.build_hihesNode_DummyPadding(hihesNode_ClosestRelativable_parent); hihesNode_Heading_prevDfs_currHierarchyDownwardChild = hihesNode_Heading_currDfs; //@flow-direction: stepping downward -> & re-loop } // @bug-guard-trivial,debug:: (dont think should happen) if (i > 80) { console.log('>> while (levelHeading_CurrElt > hihesNode_Heading_prevDfs_currHierarchySteppingDown.levelHierarchy) { -- inf looping? :: ' + i + ' - ' + hihesNode_Heading_prevDfs_currHierarchyDownwardChild); } } // while ();; } // #>>> put in shallower layer else if (headingLevel_CurrElt < hihesNode_Heading_prevDfs.headingLevel) { // eg: 2 < 4 let hihesNode_Heading_prevDfs_currHierarchyUpwardParent = hihesNode_Heading_prevDfs; //rt: // 1. if <, -> find node_parent / node_grandparent / node_grandgrandparent ... of node_prev, until appropriate, then append node_curr ... while (headingLevel_CurrElt <= hihesNode_Heading_prevDfs_currHierarchyUpwardParent.headingLevel) { // @duplicated-check: ^ hihesNode_Heading_prevDfs_currHierarchyUpwardParent = hihesNode_Heading_prevDfs_currHierarchyUpwardParent.hihesNode_parent; } let hihesNode_Heading_currDfs_builder = HihesNode.initBuilder(elt_HeadingOrNot_curr, hihesNode_Heading_prevDfs_currHierarchyUpwardParent); hihesNode_Heading_currDfs = hihesNode_Heading_currDfs_builder.build_hihesNode_HeadingOrNot_RelativeOrNot(det_IsHihesHeadingNode_currELt, det_IsHeadingLevelRelative_currElt, headingLevel_rel_currElt, hihesNode_ClosestRelativable_parent); } // @debug-info:: // hihesNode_Heading_currDfs.cssInfo_headingLevel = cssInfo_headingLevel_CurrELt; // hihesNode_Heading_currDfs.cssInfo_IsHeading = cssInfo_IsHeading_CurrELt; // @flow-direction: hihesNode_Heading_prevDfs = hihesNode_Heading_currDfs; // @note,minor:: for the naming of `_(prev|curr)Dfs`:_ // when in the normal case (/ first few times search), the prevDfs rt _the element in the prev nodeHierarchy in the Linear Loop Array_; // however (at later times, -) after you append dummy padding node, its (may) no longer in "the linear looped array" // @note,minor: the cases for prev were all elt_NotAHeading (no heading elt has come yet) is backed by the node_root } // #>- linear loop Dfs;; // // ;debug; console.log(hihesNode_root); // ;debug; console.log(hihesNode_root.toString()); return hihesNode_root; } // @helper_function of `convert_HihesNode_to_TocbotOriRequiredHeadingObject()` // convert_HihesNode_to_TocbotOriRequiredHeadingObject_individually, individually, not recursively // -- so the `children: []` will always be `[]` // // @version-modify: this correspond to the prev ori tocbot code [`getHeadingObject()` (similar)] function convert_HihesNode_to_TocbotOriRequiredHeadingObject_individually(hihesNode) { let elt_HeadingOrNot = hihesNode.elt_self; if (options.ignoreHiddenElements && (!elt_HeadingOrNot.offsetHeight || !elt_HeadingOrNot.offsetParent)) { return null; } // @main:: tocbotHeadingObject let tocbotHeadingObject; if (hihesNode.det_IsHihesDummyPaddingNode === false) { // is not a node_dummyPadding // ie: elt_Heading !== null // can be a elt_NotAHeading const headingLabel = elt_HeadingOrNot.getAttribute('data-heading-label') || (options.headingLabelCallback ? String(options.headingLabelCallback(elt_HeadingOrNot.textContent)) : elt_HeadingOrNot.textContent.trim()); // @main: elt_Heading.textContent tocbotHeadingObject = { id: elt_HeadingOrNot.id, children: [], nodeName: elt_HeadingOrNot.nodeName, headingLevel: hihesNode.headingLevel, textContent: headingLabel }; } else if (hihesNode.det_IsHihesDummyPaddingNode === true) { // is a node_dummyPadding tocbotHeadingObject = { id: null, // (dk the sematic of tocbotHeadingObject that requires ... but use this for now... // actually works) children: [], nodeName: null, headingLevel: hihesNode.headingLevel, textContent: null }; } else { throw new Error('invalid value for hihesNode.det_IsHihesDummyPaddingNode :: ' + hihesNode.det_IsHihesDummyPaddingNode); } // `hihesNode_root` will not & should not be used in this function // @main:: tocbotHeadingObject;; if (options.includeHtml) { obj.childNodes = elt_HeadingOrNot.childNodes; } if (options.headingObjectCallback) { return options.headingObjectCallback(obj, elt_HeadingOrNot); } if (tocbotHeadingObject.headingLevel >= options.collapseDepth) { tocbotHeadingObject.isCollapsed = true; } return tocbotHeadingObject; } // provided with a (root) node `hihesNode_HeadingToBeConverted_parent_currRecursionDfs` // converts the `hihesNode_HeadingToBeConverted_parent_currRecursionDfs` into a `tocbotHeadingObject` // -- a heading object tocbot originally required (before I modify the code in `convert_LinearArrayWithHeadings_to_HihesNode()` -- ie: `tocbotHeadingObject_root = { nest: [] };`) // // converts recursively, for all the nodes in that hierarchy tree (that (root) node) // returns the converted-into `tocbotHeadingObject` // // `tocbotHeadingObject_ConvertedInto_parent_currRecursionDfs` is for internal recursion -- you should not use (/-assign any value to) it function convert_HihesNode_to_TocbotOriRequiredHeadingObject(hihesNode_HeadingToBeConverted_parent_currRecursionDfs, tocbotHeadingObject_ConvertedInto_parent_currRecursionDfs = undefined) { let tocbotHeadingObject_root; let arr_tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs; // init recursion /// this must be placed outside of the loop -> otherwise, each time it loops (when its not under recursion -- the first loop) its `tocbotHeadingObject_currRecursionDfs_parentOfConvertedIntoTocbotHeadingObject` is always undefined if (tocbotHeadingObject_ConvertedInto_parent_currRecursionDfs === undefined) { tocbotHeadingObject_root = { nest: [] }; // @note: double loop is due to:_ // 1. inconsistency of structure & naming: in the root heading object -- uses `nest` vs in the child heading obj -- uses `children` // 2. hihesNode_root -- loop through (decompose) first arr_tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs = tocbotHeadingObject_root.nest; } // subsequent loop else { arr_tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs = tocbotHeadingObject_ConvertedInto_parent_currRecursionDfs.children; } // (loop + recursion::) (loop first) // @comment_ignore_this_comment [[aga, recursion is vertically, loop is horizontally let arr_hihesNode_HeadingToBeConverted_precentGen_currRecursionDfs = hihesNode_HeadingToBeConverted_parent_currRecursionDfs.arr_hihesNode_child; if (arr_hihesNode_HeadingToBeConverted_precentGen_currRecursionDfs !== null && arr_hihesNode_HeadingToBeConverted_precentGen_currRecursionDfs.length !== 0) { // null case is for elt_NotAHeading for (let hihesNode_HeadingToBeConverted_precentGen_currRecursionDsf of arr_hihesNode_HeadingToBeConverted_precentGen_currRecursionDfs) { // (business logic -- conversion::) // @comment_ignore_this_comment // [[ said, the place of recursion & business logic is diff than before... said, DFS depth first search vs depth first action ... //think many ways... // converts the node to a tocbotHeadingObject, individually - not recursively let tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs = convert_HihesNode_to_TocbotOriRequiredHeadingObject_individually(hihesNode_HeadingToBeConverted_precentGen_currRecursionDsf); // fill up the tocbotHeadingObject hierarchy -- by appending current `tocbotHeadingObject_curr` (/ `tocbotHeadingObject_ConvertedInto_currRecursionDfs`) into the `tocbotHeadingObject_parent.children` arr_tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs.push(tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs); convert_HihesNode_to_TocbotOriRequiredHeadingObject(hihesNode_HeadingToBeConverted_precentGen_currRecursionDsf, tocbotHeadingObject_ConvertedInto_precentGen_currRecursionDfs); } } return tocbotHeadingObject_root; } // @version-modify: this correspond to the prev ori tocbot code [`nestHeadingsArray(headingsArray)`] function nestHeadingsArray(headingsArray) { // console.log('>---<'); // nestHeadingsArray_NtOriReduced(headingsArray); let hihesNode_root = convert_LinearArrayWithHeadings_to_HihesNode(headingsArray); // console.log(hihesNode_root); // console.log(hihesNode_root.toString()); let tocbotHeadingObject_root = convert_HihesNode_to_TocbotOriRequiredHeadingObject(hihesNode_root); // console.log(tocbotHeadingObject_root); return tocbotHeadingObject_root; } // ################################# Nl modification for [nest heading object] end ################################# ```
Thanks for working on this!
The code looks like a lot and a bit hard for me to follow so I don't plan to include it but feel free to make another library or a blog article to discuss it and share it with the world. I took a try at expanding on the CSS way I mentioned earlier and came up with this:
.toc-link.node-name--H1 ~ .toc-list .toc-list-item .toc-link.node-name--H3,
.toc-link.node-name--H2 ~ .toc-list .toc-list-item .toc-link.node-name--H4,
.toc-link.node-name--H3 ~ .toc-list .toc-list-item .toc-link.node-name--H5 {
margin-left: 10px;
}
It should handle the display the way you described without adjusting the nesting logic or re-writing so much code. This could easily be expanded upon to support deeper nesting levels, but I'm not sure how common those cases are or if it makes sense to add code for them. Open to it though! Let me know what you think.
Closing this but feel free to comment if you are still having issues.
Question
How can I let the Headings of tocbot indent to its corresponding position?
Problem eg
normal situation:
(There is nothing wrong with this example, tocbot is behaving normally)
problematic situation
Say, now the
heading4
no longer follows aheading3
in the html file. Then here is the problem, theheading4
here is indented to same alignment as the belowheading3
. (Though this is common in some of the Toc design. ) How can I make theheading4
indent back to 1 more indentation righter thanheading3
?current behavior:
expecting behavior: