Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4182971
SelectiveSerializer.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
8 KB
Referenced Files
None
Subscribers
None
SelectiveSerializer.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
Wikimedia\Parsoid\Html2Wt
;
use
Composer\Semver\Semver
;
use
Wikimedia\Parsoid\Config\Env
;
use
Wikimedia\Parsoid\Core\DomSourceRange
;
use
Wikimedia\Parsoid\Core\SelectiveUpdateData
;
use
Wikimedia\Parsoid\DOM\Comment
;
use
Wikimedia\Parsoid\DOM\Document
;
use
Wikimedia\Parsoid\DOM\Element
;
use
Wikimedia\Parsoid\DOM\Text
;
use
Wikimedia\Parsoid\Utils\ContentUtils
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMDataUtils
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
use
Wikimedia\Parsoid\Utils\Timing
;
use
Wikimedia\Parsoid\Utils\Utils
;
use
Wikimedia\Parsoid\Utils\WTUtils
;
use
Wikimedia\Parsoid\Wikitext\Consts
;
/**
* This is a Serializer class that will compare two versions of a DOM
* and re-use the original wikitext for unmodified regions of the DOM.
* Originally this relied on special change markers inserted by the
* editor, but we now generate these ourselves using DOMDiff.
*/
class
SelectiveSerializer
{
private
Env
$env
;
private
WikitextSerializer
$wts
;
private
SelectiveUpdateData
$selserData
;
private
bool
$trace
;
public
function
__construct
(
Env
$env
,
array
$options
)
{
$this
->
env
=
$env
;
$this
->
wts
=
new
WikitextSerializer
(
$env
,
$options
);
$this
->
selserData
=
$options
[
'selserData'
];
$this
->
trace
=
$this
->
env
->
hasTraceFlag
(
'selser'
);
}
/**
* Wrap text node children of nodes with name $nodeName in <span> tags and
* compute the DSR values for those span tags.
*
* This helps DOMDiff mark up diffs of content in these nodes at a more fine-grained level.
*
* These DSR values rely on availability of information about trimmed leading
* and trailing WS in these nodes in the wt->html direction. Given this info,
* on the original unedited DOM, the computed DSR values for span tags wrapping
* text nodes will be accurate.
*
* However, for the edited DOM with modified nodes, the computation is necessarily
* speculative and as such, the computed DSR values may be bogus. Given this,
* we rely on DOMDiff to diff the data-parsoid attribute and mark these nodes as
* modified because of the mismatched dsr values. If so, these span tags will never
* have selser reuse apply to them and the speculatively computed DSR values will
* be discarded.
*
* @param Element $body
* @param string $nodeName
*/
private
function
wrapTextChildrenOfNode
(
Element
$body
,
string
$nodeName
):
void
{
// Note that while it might seem that only the first and last child need to be
// wrapped, when nested list items are added, the previously last child of
// a list item become an intermediate child in the new DOM. Without the span
// wrapper, trailing trimmed whitespace gets dropped.
$inListItem
=
isset
(
Consts
::
$HTML
[
'ListItemTags'
][
$nodeName
]
);
foreach
(
DOMCompat
::
querySelectorAll
(
$body
,
$nodeName
)
as
$elt
)
{
if
(
WTUtils
::
isLiteralHTMLNode
(
$elt
)
)
{
continue
;
}
// Skip items with about id => part of templates / extensions like Cite
// CAVEAT: In some cases, this might be bailing out a little too early.
// For example, where certain extensions might actually support nested DSR
// values inside and where <li> items in them might benefit. But, given that
// so far, such extensions are more the exception than the norm, we will take
// the easy way out here and revisit this if dirty diffs for those <li> items
// merit further action in the future.
if
(
$elt
->
hasAttribute
(
'about'
)
)
{
continue
;
}
// No point wrapping text nodes if there is no usable DSR
$eltDSR
=
DOMDataUtils
::
getDataParsoid
(
$elt
)->
dsr
??
null
;
if
(
!
Utils
::
isValidDSR
(
$eltDSR
)
)
{
continue
;
}
$doc
=
$body
->
ownerDocument
;
$firstChild
=
$c
=
$elt
->
firstChild
;
$start
=
$eltDSR
->
innerStart
();
while
(
$c
)
{
if
(
$eltDSR
&&
$c
===
$firstChild
)
{
if
(
!
$eltDSR
->
hasValidLeadingWS
()
)
{
// We don't have accurate information about the length of trimmed WS.
// So, we cannot wrap this text node with a <span>.
break
;
}
else
{
$start
+=
$eltDSR
->
leadingWS
;
}
}
$next
=
$c
->
nextSibling
;
if
(
$c
instanceof
Text
)
{
$text
=
$c
->
nodeValue
;
$len
=
strlen
(
$text
);
// Don't wrap newlines since single-line-context handling will convert these
// newlines into spaces and introduce dirty-diffs. Leaving nls outside the
// wrapped text lets it be handled as separator text and emitted appropriately.
if
(
$len
>
0
&&
$text
[
$len
-
1
]
===
"
\n
"
)
{
$text
=
rtrim
(
$text
,
"
\n
"
);
$numOfNls
=
$len
-
strlen
(
$text
);
$nl
=
str_repeat
(
"
\n
"
,
$numOfNls
);
$len
-=
$numOfNls
;
}
else
{
$nl
=
null
;
// Detect last child of "original" item and tack on trailingWS width
// to the contents of this text node. If this is a list item and
// we added a nested list, that nested list will be the last item.
//
// Note that trailingWS is only captured for the last line, so if
// the text ends in a newline (the "if" condition), we shouldn't need
// to do this.
if
(
$eltDSR
&&
(
!
$next
||
(
$inListItem
&&
DOMUtils
::
isList
(
$next
)
&&
WTUtils
::
isNewElt
(
$next
)
)
)
)
{
if
(
!
$eltDSR
->
hasValidTrailingWS
()
)
{
break
;
}
else
{
$len
+=
$eltDSR
->
trailingWS
;
}
}
}
$span
=
$doc
->
createElement
(
'span'
);
$span
->
setAttribute
(
'data-mw-selser-wrapper'
,
''
);
$dp
=
DOMDataUtils
::
getDataParsoid
(
$span
);
$dp
->
dsr
=
new
DomSourceRange
(
$start
,
$start
+
$len
,
0
,
0
);
$start
+=
$len
;
if
(
$nl
)
{
$elt
->
insertBefore
(
$span
,
$c
);
$span
->
appendChild
(
$doc
->
createTextNode
(
$text
)
);
$c
->
nodeValue
=
$nl
;
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable
$start
+=
$numOfNls
;
}
else
{
$elt
->
replaceChild
(
$span
,
$c
);
$span
->
appendChild
(
$c
);
}
}
elseif
(
$c
instanceof
Comment
)
{
$start
+=
WTUtils
::
decodedCommentLength
(
$c
);
}
elseif
(
$c
instanceof
Element
)
{
// No point wrapping following text nodes if there won't be any usable DSR
$cDSR
=
DOMDataUtils
::
getDataParsoid
(
$c
)->
dsr
??
null
;
if
(
!
Utils
::
isValidDSR
(
$cDSR
)
)
{
break
;
}
$start
=
$cDSR
->
end
;
$next
=
$c
->
hasAttribute
(
'about'
)
?
WTUtils
::
skipOverEncapsulatedContent
(
$c
)
:
$next
;
}
$c
=
$next
;
}
}
}
private
function
preprocessDOMForSelser
(
Element
$body
):
void
{
if
(
Semver
::
satisfies
(
$this
->
env
->
getInputContentVersion
(),
'>=2.1.2'
)
)
{
// Wrap text node children of <li> elements in dummy spans
$this
->
wrapTextChildrenOfNode
(
$body
,
'li'
);
$this
->
wrapTextChildrenOfNode
(
$body
,
'dd'
);
}
}
/**
* Selectively serialize an HTML DOM.
*
* WARNING: You probably want to use WikitextContentModelHandler::fromDOM instead.
*
* @param Document $doc
* @return string
*/
public
function
serializeDOM
(
Document
$doc
):
string
{
$serializeStart
=
null
;
$domDiffStart
=
null
;
$r
=
null
;
$body
=
DOMCompat
::
getBody
(
$doc
);
$oldBody
=
DOMCompat
::
getBody
(
$this
->
selserData
->
revDOM
);
// Preprocess DOMs - this is specific to selser
$this
->
preprocessDOMForSelser
(
$oldBody
);
$this
->
preprocessDOMForSelser
(
$body
);
// Use provided diff-marked DOM (used during testing)
// or generate one (used in production)
if
(
$this
->
env
->
getDOMDiff
()
)
{
$diff
=
[
'isEmpty'
=>
false
];
$body
=
DOMCompat
::
getBody
(
$this
->
env
->
getDOMDiff
()
);
}
else
{
$domDiffTiming
=
Timing
::
start
(
$this
->
env
->
getSiteConfig
()
);
$diff
=
(
new
DOMDiff
(
$this
->
env
)
)->
diff
(
$oldBody
,
$body
);
$domDiffTiming
->
end
(
'html2wt.selser.domDiff'
,
'html2wt_domDiff_seconds'
,
[
'wts'
=>
'selser'
]
);
}
if
(
$diff
[
'isEmpty'
]
)
{
// Nothing was modified, just re-use the original source
$r
=
$this
->
selserData
->
revText
;
}
else
{
if
(
$this
->
trace
||
$this
->
env
->
hasDumpFlag
(
'dom:post-dom-diff'
)
)
{
$options
=
[
'storeDiffMark'
=>
true
];
$this
->
env
->
writeDump
(
ContentUtils
::
dumpDOM
(
$oldBody
,
'OLD DOM '
,
$options
)
.
"
\n
"
.
ContentUtils
::
dumpDOM
(
$body
,
'DOM after running DOMDiff'
,
$options
)
);
}
// Call the WikitextSerializer to do our bidding
$r
=
$this
->
wts
->
serializeDOM
(
$doc
,
true
);
}
return
$r
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 20 2026, 13:57 (11 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
40/d7/620bd10d21738aaf16cdafff4367
Default Alt Text
SelectiveSerializer.php (8 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment