44
55use BookStack \Util \HtmlDocument ;
66use Closure ;
7+ use DOMDocument ;
8+ use DOMElement ;
79use DOMNode ;
810use DOMText ;
911
@@ -22,48 +24,26 @@ public function parse(): string
2224 $ doc = new HtmlDocument ($ this ->pageHtml );
2325
2426 $ tags = $ this ->locateAndIsolateIncludeTags ($ doc );
27+ $ topLevel = [...$ doc ->getBodyChildren ()];
2528
2629 foreach ($ tags as $ tag ) {
2730 $ htmlContent = $ this ->pageContentForId ->call ($ this , $ tag ->getPageId ());
2831 $ content = new PageIncludeContent ($ htmlContent , $ tag );
2932
30- if ($ content ->isInline ()) {
31- $ adopted = $ doc ->adoptNodes ($ content ->toDomNodes ());
32- foreach ($ adopted as $ adoptedContentNode ) {
33- $ tag ->domNode ->parentNode ->insertBefore ($ adoptedContentNode , $ tag ->domNode );
33+ if (!$ content ->isInline ()) {
34+ $ isParentTopLevel = in_array ($ tag ->domNode ->parentNode , $ topLevel , true );
35+ if ($ isParentTopLevel ) {
36+ $ this ->splitNodeAtChildNode ($ tag ->domNode ->parentNode , $ tag ->domNode );
37+ } else {
38+ $ this ->promoteTagNodeToBody ($ tag , $ doc ->getBody ());
3439 }
35- $ tag ->domNode ->parentNode ->removeChild ($ tag ->domNode );
36- continue ;
3740 }
3841
39- // TODO - Non-inline
42+ $ this -> replaceNodeWithNodes ( $ tag -> domNode , $ content -> toDomNodes ());
4043 }
4144
42- // TODO:
43- // Hunt down the specific text nodes with matches
44- // Split out tag text node from rest of content
45- // Fetch tag content->
46- // If range or top-block: delete tag text node, [Promote to top-block], delete old top-block if empty
47- // If inline: Replace current text node with new text or elem
48- // !! "Range" or "inline" status should come from tag parser and content fetcher, not guessed direct from content
49- // since we could have a range of inline elements
50-
51- // [Promote to top-block]
52- // Tricky operation.
53- // Can throw in before or after current top-block depending on relative position
54- // Could [Split] top-block but complex past a single level depth.
55- // Maybe [Split] if one level depth, otherwise default to before/after block
56- // Should work for the vast majority of cases, and not for those which would
57- // technically be invalid in-editor anyway.
58-
59- // [Split]
60- // Copy original top-block node type and attrs (apart from ID)
61- // Move nodes after promoted tag-node into copy
62- // Insert copy after original (after promoted top-block eventually)
63-
64- // Notes: May want to eventually parse through backwards, which should avoid issues
65- // in changes affecting the next tag, where tags may be in the same/adjacent nodes.
66-
45+ // TODO Notes: May want to eventually parse through backwards, which should avoid issues
46+ // in changes affecting the next tag, where tags may be in the same/adjacent nodes.
6747
6848 return $ doc ->getBodyInnerHtml ();
6949 }
@@ -125,4 +105,71 @@ protected function splitTextNodesAtTags(DOMNode $textNode): array
125105
126106 return $ includeTags ;
127107 }
108+
109+ /**
110+ * @param DOMNode[] $replacements
111+ */
112+ protected function replaceNodeWithNodes (DOMNode $ toReplace , array $ replacements ): void
113+ {
114+ /** @var DOMDocument $targetDoc */
115+ $ targetDoc = $ toReplace ->ownerDocument ;
116+
117+ foreach ($ replacements as $ replacement ) {
118+ if ($ replacement ->ownerDocument !== $ targetDoc ) {
119+ $ replacement = $ targetDoc ->adoptNode ($ replacement );
120+ }
121+
122+ $ toReplace ->parentNode ->insertBefore ($ replacement , $ toReplace );
123+ }
124+
125+ $ toReplace ->parentNode ->removeChild ($ toReplace );
126+ }
127+
128+ protected function promoteTagNodeToBody (PageIncludeTag $ tag , DOMNode $ body ): void
129+ {
130+ /** @var DOMNode $topParent */
131+ $ topParent = $ tag ->domNode ->parentNode ;
132+ while ($ topParent ->parentNode !== $ body ) {
133+ $ topParent = $ topParent ->parentNode ;
134+ }
135+
136+ $ parentText = $ topParent ->textContent ;
137+ $ tagPos = strpos ($ parentText , $ tag ->tagContent );
138+ $ before = $ tagPos < (strlen ($ parentText ) / 2 );
139+
140+ if ($ before ) {
141+ $ body ->insertBefore ($ tag ->domNode , $ topParent );
142+ } else {
143+ $ body ->insertBefore ($ tag ->domNode , $ topParent ->nextSibling );
144+ }
145+ }
146+
147+ protected function splitNodeAtChildNode (DOMElement $ parentNode , DOMNode $ domNode ): void
148+ {
149+ $ children = [...$ parentNode ->childNodes ];
150+ $ splitPos = array_search ($ domNode , $ children , true ) ?: count ($ children );
151+ $ parentClone = $ parentNode ->cloneNode ();
152+ $ parentClone ->removeAttribute ('id ' );
153+
154+ /** @var DOMNode $child */
155+ for ($ i = 0 ; $ i < $ splitPos ; $ i ++) {
156+ $ child = $ children [0 ];
157+ $ parentClone ->appendChild ($ child );
158+ }
159+
160+ if ($ parentClone ->hasChildNodes ()) {
161+ $ parentNode ->parentNode ->insertBefore ($ parentClone , $ parentNode );
162+ }
163+
164+ $ parentNode ->parentNode ->insertBefore ($ domNode , $ parentNode );
165+
166+ $ parentClone ->normalize ();
167+ $ parentNode ->normalize ();
168+ if (!$ parentNode ->hasChildNodes ()) {
169+ $ parentNode ->remove ();
170+ }
171+ if (!$ parentClone ->hasChildNodes ()) {
172+ $ parentClone ->remove ();
173+ }
174+ }
128175}
0 commit comments