Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4122818
References.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
29 KB
Referenced Files
None
Subscribers
None
References.php
View Options
<?php
declare
(
strict_types
=
1
);
// phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment
// @phan-file-suppress MediaWikiNoEmptyIfDefined Phan thinks the @property docs in the DataParsoid
// class are a guarantee, but they aren't
namespace
Cite\Parsoid
;
use
Closure
;
use
MediaWiki\Config\Config
;
use
stdClass
;
use
Wikimedia\Parsoid\Core\DomSourceRange
;
use
Wikimedia\Parsoid\DOM\DocumentFragment
;
use
Wikimedia\Parsoid\DOM\Element
;
use
Wikimedia\Parsoid\DOM\Node
;
use
Wikimedia\Parsoid\Ext\DOMDataUtils
;
use
Wikimedia\Parsoid\Ext\DOMUtils
;
use
Wikimedia\Parsoid\Ext\ExtensionTagHandler
;
use
Wikimedia\Parsoid\Ext\ParsoidExtensionAPI
;
use
Wikimedia\Parsoid\Ext\PHPUtils
;
use
Wikimedia\Parsoid\Ext\WTUtils
;
use
Wikimedia\Parsoid\NodeData\DataMw
;
use
Wikimedia\Parsoid\NodeData\DataMwError
;
use
Wikimedia\Parsoid\NodeData\DataParsoid
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
/**
* @license GPL-2.0-or-later
*/
class
References
extends
ExtensionTagHandler
{
private
Config
$mainConfig
;
public
function
__construct
(
Config
$mainConfig
)
{
$this
->
mainConfig
=
$mainConfig
;
}
private
static
function
hasRef
(
Node
$node
):
bool
{
$c
=
$node
->
firstChild
;
while
(
$c
)
{
if
(
$c
instanceof
Element
)
{
if
(
WTUtils
::
isSealedFragmentOfType
(
$c
,
'ref'
)
)
{
return
true
;
}
if
(
self
::
hasRef
(
$c
)
)
{
return
true
;
}
}
$c
=
$c
->
nextSibling
;
}
return
false
;
}
private
function
createReferences
(
ParsoidExtensionAPI
$extApi
,
DocumentFragment
$domFragment
,
array
$refsOpts
,
?
callable
$modifyDp
,
bool
$autoGenerated
=
false
):
Element
{
$doc
=
$domFragment
->
ownerDocument
;
$ol
=
$doc
->
createElement
(
'ol'
);
DOMCompat
::
getClassList
(
$ol
)->
add
(
'mw-references'
,
'references'
);
DOMUtils
::
migrateChildren
(
$domFragment
,
$ol
);
// Support the `responsive` parameter
if
(
$refsOpts
[
'responsive'
]
!==
null
)
{
$responsiveWrap
=
$refsOpts
[
'responsive'
]
!==
'0'
;
}
else
{
$responsiveWrap
=
(
bool
)
$this
->
mainConfig
->
get
(
'CiteResponsiveReferences'
);
}
if
(
$responsiveWrap
)
{
$div
=
$doc
->
createElement
(
'div'
);
DOMCompat
::
getClassList
(
$div
)->
add
(
'mw-references-wrap'
);
$div
->
appendChild
(
$ol
);
$frag
=
$div
;
}
else
{
$frag
=
$ol
;
}
if
(
$autoGenerated
)
{
// FIXME: This is very much trying to copy ExtensionHandler::onDocument
DOMUtils
::
addAttributes
(
$frag
,
[
'typeof'
=>
'mw:Extension/references'
,
'about'
=>
$extApi
->
newAboutId
()
]
);
$dataMw
=
new
DataMw
(
[
'name'
=>
'references'
,
'attrs'
=>
new
stdClass
]
);
// Dont emit empty keys
if
(
$refsOpts
[
'group'
]
)
{
$dataMw
->
attrs
->
group
=
$refsOpts
[
'group'
];
}
DOMDataUtils
::
setDataMw
(
$frag
,
$dataMw
);
}
$dp
=
DOMDataUtils
::
getDataParsoid
(
$frag
);
if
(
$refsOpts
[
'group'
]
)
{
// No group for the empty string either
$dp
->
group
=
$refsOpts
[
'group'
];
$ol
->
setAttribute
(
'data-mw-group'
,
$refsOpts
[
'group'
]
);
}
if
(
$modifyDp
)
{
$modifyDp
(
$dp
);
}
// These module namess are copied from Cite extension.
// They are hardcoded there as well.
$metadata
=
$extApi
->
getMetadata
();
$metadata
->
addModules
(
[
'ext.cite.ux-enhancements'
]
);
$metadata
->
addModuleStyles
(
[
'ext.cite.parsoid.styles'
,
'ext.cite.styles'
]
);
return
$frag
;
}
private
function
extractRefFromNode
(
ParsoidExtensionAPI
$extApi
,
Element
$node
,
ReferencesData
$refsData
):
void
{
$doc
=
$node
->
ownerDocument
;
$errs
=
[];
// This is data-parsoid from the dom fragment node that's gone through
// dsr computation and template wrapping.
$nodeDp
=
DOMDataUtils
::
getDataParsoid
(
$node
);
$isTplWrapper
=
DOMUtils
::
hasTypeOf
(
$node
,
'mw:Transclusion'
);
$contentId
=
$nodeDp
->
html
;
$tplDmw
=
$isTplWrapper
?
DOMDataUtils
::
getDataMw
(
$node
)
:
null
;
// This is the <sup> that's the meat of the sealed fragment
$c
=
$extApi
->
getContentDOM
(
$contentId
)->
firstChild
;
DOMUtils
::
assertElt
(
$c
);
$cDp
=
DOMDataUtils
::
getDataParsoid
(
$c
);
$refDmw
=
DOMDataUtils
::
getDataMw
(
$c
);
// Use the about attribute on the wrapper with priority, since it's
// only added when the wrapper is a template sibling.
$about
=
DOMCompat
::
getAttribute
(
$node
,
'about'
)
??
DOMCompat
::
getAttribute
(
$c
,
'about'
);
'@phan-var string $about'
;
// assert that $about is non-null
// FIXME(SSS): Need to clarify semantics here.
// If both the containing <references> elt as well as the nested <ref>
// elt has a group attribute, what takes precedence?
$groupName
=
$refDmw
->
attrs
->
group
??
$refsData
->
referencesGroup
;
$group
=
$refsData
->
getRefGroup
(
$groupName
);
if
(
$refsData
->
inReferencesContent
()
&&
$groupName
!==
$refsData
->
referencesGroup
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_references_group_mismatch'
,
[
$refDmw
->
attrs
->
group
]
);
}
// NOTE: This will have been trimmed in Utils::getExtArgInfo()'s call
// to TokenUtils::kvToHash() and ExtensionHandler::normalizeExtOptions()
$refName
=
$refDmw
->
attrs
->
name
??
''
;
$followName
=
$refDmw
->
attrs
->
follow
??
''
;
$refDir
=
strtolower
(
$refDmw
->
attrs
->
dir
??
''
);
// Add ref-index linkback
$linkBack
=
$doc
->
createElement
(
'sup'
);
$ref
=
null
;
$hasRefName
=
strlen
(
$refName
)
>
0
;
$hasFollow
=
strlen
(
$followName
)
>
0
;
$validFollow
=
false
;
if
(
$hasFollow
)
{
// Always wrap follows content so that there's no ambiguity
// where to find it when roundtripping
$span
=
$doc
->
createElement
(
'span'
);
DOMUtils
::
addTypeOf
(
$span
,
'mw:Cite/Follow'
);
$span
->
setAttribute
(
'about'
,
$about
);
$span
->
appendChild
(
$doc
->
createTextNode
(
' '
)
);
DOMUtils
::
migrateChildren
(
$c
,
$span
);
$c
->
appendChild
(
$span
);
}
$html
=
''
;
$contentDiffers
=
false
;
if
(
$hasRefName
)
{
if
(
$hasFollow
)
{
// Presumably, "name" has higher precedence
$errs
[]
=
new
DataMwError
(
'cite_error_ref_follow_conflicts'
);
}
if
(
isset
(
$group
->
indexByName
[
$refName
]
)
)
{
$ref
=
$group
->
indexByName
[
$refName
];
// If there are multiple <ref>s with the same name, but different content,
// the content of the first <ref> shows up in the <references> section.
// in order to ensure lossless RT-ing for later <refs>, we have to record
// HTML inline for all of them.
if
(
$ref
->
contentId
)
{
if
(
$ref
->
cachedHtml
===
null
)
{
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
$refContent
=
$extApi
->
getContentDOM
(
$ref
->
contentId
)->
firstChild
;
$ref
->
cachedHtml
=
$extApi
->
domToHtml
(
$refContent
,
true
,
false
);
}
// See the test, "Forward-referenced ref with magical follow edge case"
// Ideally, we should strip the mw:Cite/Follow wrappers before comparing
// But, we are going to ignore this edge case as not worth the complexity.
$html
=
$extApi
->
domToHtml
(
$c
,
true
,
false
);
$contentDiffers
=
(
$html
!==
$ref
->
cachedHtml
);
}
}
else
{
if
(
$refsData
->
inReferencesContent
()
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_references_missing_key'
,
[
$refDmw
->
attrs
->
name
]
);
}
}
}
else
{
if
(
$hasFollow
)
{
// This is a follows ref, so check that a named ref has already
// been defined
if
(
isset
(
$group
->
indexByName
[
$followName
]
)
)
{
$validFollow
=
true
;
$ref
=
$group
->
indexByName
[
$followName
];
}
else
{
// FIXME: This key isn't exactly appropriate since this
// is more general than just being in a <references>
// section and it's the $followName we care about, but the
// extension to the legacy parser doesn't have an
// equivalent key and just outputs something wacky.
$errs
[]
=
new
DataMwError
(
'cite_error_references_missing_key'
,
[
$refDmw
->
attrs
->
follow
]
);
}
}
elseif
(
$refsData
->
inReferencesContent
()
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_references_no_key'
);
}
}
// Process nested ref-in-ref
//
// Do this before possibly adding the a ref below or
// migrating contents out of $c if we have a valid follow
if
(
empty
(
$cDp
->
empty
)
&&
self
::
hasRef
(
$c
)
)
{
if
(
$contentDiffers
)
{
$refsData
->
pushEmbeddedContentFlag
();
}
$this
->
processRefs
(
$extApi
,
$refsData
,
$c
);
if
(
$contentDiffers
)
{
$refsData
->
popEmbeddedContentFlag
();
// If we have refs and the content differs, we need to
// reserialize now that we processed the refs. Unfortunately,
// the cachedHtml we compared against already had its refs
// processed so that would presumably never match and this will
// always be considered a redefinition. The implementation for
// the legacy parser also considers this a redefinition so
// there is likely little content out there like this :)
$html
=
$extApi
->
domToHtml
(
$c
,
true
,
true
);
}
}
if
(
$validFollow
)
{
// Migrate content from the follow to the ref
if
(
$ref
->
contentId
)
{
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
$refContent
=
$extApi
->
getContentDOM
(
$ref
->
contentId
)->
firstChild
;
DOMUtils
::
migrateChildren
(
$c
,
$refContent
);
}
else
{
// Otherwise, we have a follow that comes after a named
// ref without content so use the follow fragment as
// the content
// This will be set below with `$ref->contentId = $contentId;`
}
}
else
{
// If we have !$ref, one might have been added in the call to
// processRefs, ie. a self-referential ref. We could try to look
// it up again, but Parsoid is choosing not to support that.
// Even worse would be if it tried to redefine itself!
if
(
!
$ref
)
{
$ref
=
$refsData
->
add
(
$extApi
,
$groupName
,
$refName
,
$refDir
);
}
// Handle linkbacks
if
(
$refsData
->
inEmbeddedContent
()
)
{
$ref
->
embeddedNodes
[]
=
$about
;
}
else
{
$ref
->
nodes
[]
=
$linkBack
;
$ref
->
linkbacks
[]
=
$ref
->
key
.
'-'
.
count
(
$ref
->
linkbacks
);
}
}
if
(
isset
(
$refDmw
->
attrs
->
dir
)
)
{
if
(
$refDir
!==
'rtl'
&&
$refDir
!==
'ltr'
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_ref_invalid_dir'
,
[
$refDir
]
);
}
elseif
(
$ref
->
dir
!==
''
&&
$ref
->
dir
!==
$refDir
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_ref_conflicting_dir'
,
[
$ref
->
name
]
);
}
}
// FIXME: At some point this error message can be changed to a warning, as Parsoid Cite now
// supports numerals as a name without it being an actual error, but core Cite does not.
// Follow refs do not duplicate the error which can be correlated with the original ref.
if
(
ctype_digit
(
$refName
)
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_ref_numeric_key'
);
}
// Check for missing content, added ?? '' to fix T259676 crasher
// FIXME: See T260082 for a more complete description of cause and deeper fix
$missingContent
=
(
!
empty
(
$cDp
->
empty
)
||
trim
(
$refDmw
->
body
->
extsrc
??
''
)
===
''
);
if
(
$missingContent
)
{
// Check for missing name and content to generate error code
//
// In references content, refs should be used for definition so missing content
// is an error. It's possible that no name is present (!hasRefName), which also
// gets the error "cite_error_references_no_key" above, so protect against that.
if
(
$refsData
->
inReferencesContent
()
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_empty_references_define'
,
[
$refDmw
->
attrs
->
name
??
''
,
$refDmw
->
attrs
->
group
??
''
]
);
}
elseif
(
!
$hasRefName
)
{
if
(
!
empty
(
$cDp
->
selfClose
)
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_ref_no_key'
);
}
else
{
$errs
[]
=
new
DataMwError
(
'cite_error_ref_no_input'
);
}
}
if
(
!
empty
(
$cDp
->
selfClose
)
)
{
unset
(
$refDmw
->
body
);
}
else
{
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat
::
replaceChildren
(
$c
);
$refDmw
->
body
=
(
object
)[
'html'
=>
$refDmw
->
body
->
extsrc
??
''
];
}
}
else
{
if
(
$ref
->
contentId
&&
!
$validFollow
)
{
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat
::
replaceChildren
(
$c
);
}
if
(
$contentDiffers
)
{
// TODO: Since this error is being placed on the ref, the
// key should arguably be "cite_error_ref_duplicate_key"
$errs
[]
=
new
DataMwError
(
'cite_error_references_duplicate_key'
,
[
$refDmw
->
attrs
->
name
]
);
$refDmw
->
body
=
(
object
)[
'html'
=>
$html
];
}
else
{
$refDmw
->
body
=
(
object
)[
'id'
=>
'mw-reference-text-'
.
$ref
->
target
];
}
}
$class
=
'mw-ref reference'
;
if
(
$validFollow
)
{
$class
.=
' mw-ref-follow'
;
}
$lastLinkback
=
$ref
->
linkbacks
[
count
(
$ref
->
linkbacks
)
-
1
]
??
null
;
DOMUtils
::
addAttributes
(
$linkBack
,
[
'about'
=>
$about
,
'class'
=>
$class
,
'id'
=>
(
$refsData
->
inEmbeddedContent
()
||
$validFollow
)
?
null
:
(
$ref
->
name
?
$lastLinkback
:
$ref
->
id
),
'rel'
=>
'dc:references'
,
'typeof'
=>
DOMCompat
::
getAttribute
(
$node
,
'typeof'
),
]
);
DOMUtils
::
removeTypeOf
(
$linkBack
,
'mw:DOMFragment/sealed/ref'
);
DOMUtils
::
addTypeOf
(
$linkBack
,
'mw:Extension/ref'
);
$dataParsoid
=
new
DataParsoid
;
if
(
isset
(
$nodeDp
->
src
)
)
{
$dataParsoid
->
src
=
$nodeDp
->
src
;
}
if
(
isset
(
$nodeDp
->
dsr
)
)
{
$dataParsoid
->
dsr
=
$nodeDp
->
dsr
;
}
if
(
isset
(
$nodeDp
->
pi
)
)
{
$dataParsoid
->
pi
=
$nodeDp
->
pi
;
}
DOMDataUtils
::
setDataParsoid
(
$linkBack
,
$dataParsoid
);
$dmw
=
$isTplWrapper
?
$tplDmw
:
$refDmw
;
DOMDataUtils
::
setDataMw
(
$linkBack
,
$dmw
);
// FIXME(T214241): Should the errors be added to data-mw if
// $isTplWrapper? Here and other calls to addErrorsToNode.
if
(
$errs
)
{
self
::
addErrorsToNode
(
$linkBack
,
$errs
);
}
// refLink is the link to the citation
$refLink
=
$doc
->
createElement
(
'a'
);
DOMUtils
::
addAttributes
(
$refLink
,
[
'href'
=>
$extApi
->
getPageUri
()
.
'#'
.
$ref
->
target
,
'style'
=>
'counter-reset: mw-Ref '
.
$ref
->
groupIndex
.
';'
,
]
);
if
(
$ref
->
group
)
{
$refLink
->
setAttribute
(
'data-mw-group'
,
$ref
->
group
);
}
// refLink-span which will contain a default rendering of the cite link
// for browsers that don't support counters
$refLinkSpan
=
$doc
->
createElement
(
'span'
);
$refLinkSpan
->
setAttribute
(
'class'
,
'mw-reflink-text'
);
$refLinkSpan
->
appendChild
(
$doc
->
createTextNode
(
'['
.
(
$ref
->
group
?
$ref
->
group
.
' '
:
''
)
.
$ref
->
groupIndex
.
']'
)
);
$refLink
->
appendChild
(
$refLinkSpan
);
$linkBack
->
appendChild
(
$refLink
);
// Checking if the <ref> is nested in a link
$aParent
=
DOMUtils
::
findAncestorOfName
(
$node
,
'a'
);
if
(
$aParent
!==
null
)
{
// If we find a parent link, we hoist the reference up, just after the link
// But if there's multiple references in a single link, we want to insert in order -
// so we look for other misnested references before inserting
$insertionPoint
=
$aParent
->
nextSibling
;
while
(
$insertionPoint
instanceof
Element
&&
DOMCompat
::
nodeName
(
$insertionPoint
)
===
'sup'
&&
!
empty
(
DOMDataUtils
::
getDataParsoid
(
$insertionPoint
)->
misnested
)
)
{
$insertionPoint
=
$insertionPoint
->
nextSibling
;
}
$aParent
->
parentNode
->
insertBefore
(
$linkBack
,
$insertionPoint
);
// set misnested to true and DSR to zero-sized to avoid round-tripping issues
$dsrOffset
=
DOMDataUtils
::
getDataParsoid
(
$aParent
)->
dsr
->
end
??
null
;
// we created that node hierarchy above, so we know that it only contains these nodes,
// hence there's no need for a visitor
self
::
setMisnested
(
$linkBack
,
$dsrOffset
);
self
::
setMisnested
(
$refLink
,
$dsrOffset
);
self
::
setMisnested
(
$refLinkSpan
,
$dsrOffset
);
$parentAbout
=
DOMCompat
::
getAttribute
(
$aParent
,
'about'
);
if
(
$parentAbout
!==
null
)
{
$linkBack
->
setAttribute
(
'about'
,
$parentAbout
);
}
$node
->
parentNode
->
removeChild
(
$node
);
}
else
{
// if not, we insert it where we planned in the first place
$node
->
parentNode
->
replaceChild
(
$linkBack
,
$node
);
}
// Keep the first content to compare multiple <ref>s with the same name.
if
(
$ref
->
contentId
===
null
&&
!
$missingContent
)
{
$ref
->
contentId
=
$contentId
;
// Use the dir parameter only from the full definition of a named ref tag
$ref
->
dir
=
$refDir
;
}
else
{
DOMCompat
::
remove
(
$c
);
$extApi
->
clearContentDOM
(
$contentId
);
}
}
/**
* Sets a node as misnested and its DSR as zero-width.
*/
private
static
function
setMisnested
(
Element
$node
,
?
int
$offset
)
{
$dataParsoid
=
DOMDataUtils
::
getDataParsoid
(
$node
);
$dataParsoid
->
misnested
=
true
;
$dataParsoid
->
dsr
=
new
DomSourceRange
(
$offset
,
$offset
,
null
,
null
);
}
/**
* @param Element $node
* @param list<DataMwError> $errs
*/
private
static
function
addErrorsToNode
(
Element
$node
,
array
$errs
):
void
{
DOMUtils
::
addTypeOf
(
$node
,
'mw:Error'
);
$dmw
=
DOMDataUtils
::
getDataMw
(
$node
);
$dmw
->
errors
=
is_array
(
$dmw
->
errors
??
null
)
?
array_merge
(
$dmw
->
errors
,
$errs
)
:
$errs
;
}
private
function
insertReferencesIntoDOM
(
ParsoidExtensionAPI
$extApi
,
Element
$refsNode
,
ReferencesData
$refsData
,
bool
$autoGenerated
=
false
):
void
{
$isTplWrapper
=
DOMUtils
::
hasTypeOf
(
$refsNode
,
'mw:Transclusion'
);
$dp
=
DOMDataUtils
::
getDataParsoid
(
$refsNode
);
$group
=
$dp
->
group
??
''
;
$refGroup
=
$refsData
->
getRefGroup
(
$group
);
// Iterate through the ref list to back-patch typeof and data-mw error
// information into ref for errors only known at time of references
// insertion. Refs in the top level dom will be processed immediately,
// whereas embedded refs will be gathered for batch processing, since
// we need to parse embedded content to find them.
if
(
$refGroup
)
{
$autoGeneratedWithGroup
=
(
$autoGenerated
&&
$group
!==
''
);
foreach
(
$refGroup
->
refs
as
$ref
)
{
$errs
=
[];
// Mark all refs that are part of a group that is autogenerated
if
(
$autoGeneratedWithGroup
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_group_refs_without_references'
,
[
$group
]
);
}
// Mark all refs that are named without content
if
(
(
$ref
->
name
!==
''
)
&&
$ref
->
contentId
===
null
)
{
// TODO: Since this error is being placed on the ref,
// the key should arguably be "cite_error_ref_no_text"
$errs
[]
=
new
DataMwError
(
'cite_error_references_no_text'
,
[
$ref
->
name
]
);
}
if
(
$errs
)
{
foreach
(
$ref
->
nodes
as
$node
)
{
self
::
addErrorsToNode
(
$node
,
$errs
);
}
foreach
(
$ref
->
embeddedNodes
as
$about
)
{
$refsData
->
embeddedErrors
[
$about
]
=
$errs
;
}
}
}
}
// Note that `$sup`s here are probably all we really need to check for
// errors caught with `$refsData->inReferencesContent()` but it's
// probably easier to just know that state while they're being
// constructed.
$nestedRefsHTML
=
array_map
(
static
function
(
Element
$sup
)
use
(
$extApi
)
{
return
$extApi
->
domToHtml
(
$sup
,
false
,
true
)
.
"
\n
"
;
},
PHPUtils
::
iterable_to_array
(
DOMCompat
::
querySelectorAll
(
$refsNode
,
'sup[typeof~=
\'
mw:Extension/ref
\'
]'
)
)
);
if
(
!
$isTplWrapper
)
{
$dataMw
=
DOMDataUtils
::
getDataMw
(
$refsNode
);
// Mark this auto-generated so that we can skip this during
// html -> wt and so that clients can strip it if necessary.
if
(
$autoGenerated
)
{
$dataMw
->
autoGenerated
=
true
;
}
elseif
(
$nestedRefsHTML
)
{
$dataMw
->
body
=
(
object
)[
'html'
=>
"
\n
"
.
implode
(
$nestedRefsHTML
)
];
}
elseif
(
empty
(
$dp
->
selfClose
)
)
{
$dataMw
->
body
=
(
object
)[
'html'
=>
''
];
}
else
{
unset
(
$dataMw
->
body
);
}
unset
(
$dp
->
selfClose
);
}
// Deal with responsive wrapper
if
(
DOMUtils
::
hasClass
(
$refsNode
,
'mw-references-wrap'
)
)
{
// NOTE: The default Cite implementation hardcodes this threshold to 10.
// We use a configurable parameter here primarily for test coverage purposes.
// See citeParserTests.txt where we set a threshold of 1 or 2.
$rrThreshold
=
$this
->
mainConfig
->
get
(
'CiteResponsiveReferencesThreshold'
)
??
10
;
if
(
$refGroup
&&
count
(
$refGroup
->
refs
)
>
$rrThreshold
)
{
DOMCompat
::
getClassList
(
$refsNode
)->
add
(
'mw-references-columns'
);
}
$refsNode
=
$refsNode
->
firstChild
;
}
DOMUtils
::
assertElt
(
$refsNode
);
// Remove all children from the references node
//
// Ex: When {{Reflist}} is reused from the cache, it comes with
// a bunch of references as well. We have to remove all those cached
// references before generating fresh references.
DOMCompat
::
replaceChildren
(
$refsNode
);
if
(
$refGroup
)
{
foreach
(
$refGroup
->
refs
as
$ref
)
{
$refGroup
->
renderLine
(
$extApi
,
$refsNode
,
$ref
);
}
}
// Remove the group from refsData
$refsData
->
removeRefGroup
(
$group
);
}
/**
* Process `<ref>`s left behind after the DOM is fully processed.
* We process them as if there was an implicit `<references />` tag at
* the end of the DOM.
*
* @param ParsoidExtensionAPI $extApi
* @param ReferencesData $refsData
* @param Node $node
*/
public
function
insertMissingReferencesIntoDOM
(
ParsoidExtensionAPI
$extApi
,
ReferencesData
$refsData
,
Node
$node
):
void
{
$doc
=
$node
->
ownerDocument
;
foreach
(
$refsData
->
getRefGroups
()
as
$groupName
=>
$refsGroup
)
{
$domFragment
=
$doc
->
createDocumentFragment
();
$frag
=
$this
->
createReferences
(
$extApi
,
$domFragment
,
[
// Force string cast here since in the foreach above, $groupName
// is an array key. In that context, number-like strings are
// silently converted to a numeric value!
// Ex: In <ref group="2" />, the "2" becomes 2 in the foreach
'group'
=>
(
string
)
$groupName
,
'responsive'
=>
null
,
],
static
function
(
$dp
)
use
(
$extApi
)
{
// The new references come out of "nowhere", so to make selser work
// properly, add a zero-sized DSR pointing to the end of the document.
$content
=
$extApi
->
getPageConfig
()->
getRevisionContent
()->
getContent
(
'main'
);
$contentLength
=
strlen
(
$content
);
$dp
->
dsr
=
new
DomSourceRange
(
$contentLength
,
$contentLength
,
0
,
0
);
},
true
);
// Add a \n before the <ol> so that when serialized to wikitext,
// each <references /> tag appears on its own line.
$node
->
appendChild
(
$doc
->
createTextNode
(
"
\n
"
)
);
$node
->
appendChild
(
$frag
);
$this
->
insertReferencesIntoDOM
(
$extApi
,
$frag
,
$refsData
,
true
);
}
}
private
function
processEmbeddedRefs
(
ParsoidExtensionAPI
$extApi
,
ReferencesData
$refsData
,
string
$str
):
string
{
$domFragment
=
$extApi
->
htmlToDom
(
$str
);
$this
->
processRefs
(
$extApi
,
$refsData
,
$domFragment
);
return
$extApi
->
domToHtml
(
$domFragment
,
true
,
true
);
}
public
function
processRefs
(
ParsoidExtensionAPI
$extApi
,
ReferencesData
$refsData
,
Node
$node
):
void
{
$child
=
$node
->
firstChild
;
while
(
$child
!==
null
)
{
$nextChild
=
$child
->
nextSibling
;
if
(
$child
instanceof
Element
)
{
if
(
WTUtils
::
isSealedFragmentOfType
(
$child
,
'ref'
)
)
{
$this
->
extractRefFromNode
(
$extApi
,
$child
,
$refsData
);
}
elseif
(
DOMUtils
::
hasTypeOf
(
$child
,
'mw:Extension/references'
)
)
{
if
(
!
$refsData
->
inReferencesContent
()
)
{
$refsData
->
referencesGroup
=
DOMDataUtils
::
getDataParsoid
(
$child
)->
group
??
''
;
}
$refsData
->
pushEmbeddedContentFlag
(
'references'
);
if
(
$child
->
hasChildNodes
()
)
{
$this
->
processRefs
(
$extApi
,
$refsData
,
$child
);
}
$refsData
->
popEmbeddedContentFlag
();
if
(
!
$refsData
->
inReferencesContent
()
)
{
$refsData
->
referencesGroup
=
''
;
$this
->
insertReferencesIntoDOM
(
$extApi
,
$child
,
$refsData
,
false
);
}
}
else
{
$refsData
->
pushEmbeddedContentFlag
();
// Look for <ref>s embedded in data attributes
$extApi
->
processAttributeEmbeddedHTML
(
$child
,
function
(
string
$html
)
use
(
$extApi
,
$refsData
)
{
return
$this
->
processEmbeddedRefs
(
$extApi
,
$refsData
,
$html
);
}
);
$refsData
->
popEmbeddedContentFlag
();
if
(
$child
->
hasChildNodes
()
)
{
$this
->
processRefs
(
$extApi
,
$refsData
,
$child
);
}
}
}
$child
=
$nextChild
;
}
}
/**
* Traverse into all the embedded content and mark up the refs in there
* that have errors that weren't known before the content was serialized.
*
* Some errors are only known at the time when we're inserting the
* references lists, at which point, embedded content has already been
* serialized and stored, so we no longer have live access to it. We
* therefore map about ids to errors for a ref at that time, and then do
* one final walk of the dom to peak into all the embedded content and
* mark up the errors where necessary.
*
* @param ParsoidExtensionAPI $extApi
* @param ReferencesData $refsData
* @param Node $node
*/
public
function
addEmbeddedErrors
(
ParsoidExtensionAPI
$extApi
,
ReferencesData
$refsData
,
Node
$node
):
void
{
$processEmbeddedErrors
=
function
(
string
$html
)
use
(
$extApi
,
$refsData
)
{
// Similar to processEmbeddedRefs
$domFragment
=
$extApi
->
htmlToDom
(
$html
);
$this
->
addEmbeddedErrors
(
$extApi
,
$refsData
,
$domFragment
);
return
$extApi
->
domToHtml
(
$domFragment
,
true
,
true
);
};
$processBodyHtml
=
static
function
(
Element
$n
)
use
(
$processEmbeddedErrors
)
{
$dataMw
=
DOMDataUtils
::
getDataMw
(
$n
);
if
(
isset
(
$dataMw
->
body
->
html
)
)
{
$dataMw
->
body
->
html
=
$processEmbeddedErrors
(
$dataMw
->
body
->
html
);
}
};
$child
=
$node
->
firstChild
;
while
(
$child
!==
null
)
{
$nextChild
=
$child
->
nextSibling
;
if
(
$child
instanceof
Element
)
{
if
(
DOMUtils
::
hasTypeOf
(
$child
,
'mw:Extension/ref'
)
)
{
$processBodyHtml
(
$child
);
$about
=
DOMCompat
::
getAttribute
(
$child
,
'about'
);
'@phan-var string $about'
;
// assert $about is non-null
$errs
=
$refsData
->
embeddedErrors
[
$about
]
??
null
;
if
(
$errs
)
{
self
::
addErrorsToNode
(
$child
,
$errs
);
}
}
elseif
(
DOMUtils
::
hasTypeOf
(
$child
,
'mw:Extension/references'
)
)
{
$processBodyHtml
(
$child
);
}
else
{
$extApi
->
processAttributeEmbeddedHTML
(
$child
,
$processEmbeddedErrors
);
}
if
(
$child
->
hasChildNodes
()
)
{
$this
->
addEmbeddedErrors
(
$extApi
,
$refsData
,
$child
);
}
}
$child
=
$nextChild
;
}
}
/** @inheritDoc */
public
function
sourceToDom
(
ParsoidExtensionAPI
$extApi
,
string
$txt
,
array
$extArgs
):
DocumentFragment
{
$domFragment
=
$extApi
->
extTagToDOM
(
$extArgs
,
$txt
,
[
'parseOpts'
=>
[
'extTag'
=>
'references'
],
]
);
$refsOpts
=
$extApi
->
extArgsToArray
(
$extArgs
)
+
[
'group'
=>
null
,
'responsive'
=>
null
,
];
// Detect invalid parameters on the references tag
$knownAttributes
=
[
'group'
,
'responsive'
];
foreach
(
$refsOpts
as
$key
=>
$value
)
{
if
(
!
in_array
(
strtolower
(
(
string
)
$key
),
$knownAttributes
,
true
)
)
{
$extApi
->
pushError
(
'cite_error_references_invalid_parameters'
);
break
;
}
}
$frag
=
$this
->
createReferences
(
$extApi
,
$domFragment
,
$refsOpts
,
static
function
(
$dp
)
use
(
$extApi
)
{
$dp
->
src
=
$extApi
->
extTag
->
getSource
();
// Setting redundant info on fragment.
// $docBody->firstChild info feels cumbersome to use downstream.
if
(
$extApi
->
extTag
->
isSelfClosed
()
)
{
$dp
->
selfClose
=
true
;
}
}
);
$domFragment
->
appendChild
(
$frag
);
return
$domFragment
;
}
/** @inheritDoc */
public
function
processAttributeEmbeddedHTML
(
ParsoidExtensionAPI
$extApi
,
Element
$elt
,
Closure
$proc
):
void
{
$dataMw
=
DOMDataUtils
::
getDataMw
(
$elt
);
if
(
isset
(
$dataMw
->
body
->
html
)
)
{
$dataMw
->
body
->
html
=
$proc
(
$dataMw
->
body
->
html
);
}
}
/** @inheritDoc */
public
function
domToWikitext
(
ParsoidExtensionAPI
$extApi
,
Element
$node
,
bool
$wrapperUnmodified
)
{
$dataMw
=
DOMDataUtils
::
getDataMw
(
$node
);
// Autogenerated references aren't considered erroneous (the extension to the legacy
// parser also generates them) and are not suppressed when serializing because apparently
// that's the behaviour Parsoid clients want. However, autogenerated references *with
// group attributes* are errors (the legacy extension doesn't generate them at all) and
// are suppressed when serialized since we considered them an error while parsing and
// don't want them to persist in the content.
if
(
!
empty
(
$dataMw
->
autoGenerated
)
&&
(
$dataMw
->
attrs
->
group
??
''
)
!==
''
)
{
return
''
;
}
else
{
$startTagSrc
=
$extApi
->
extStartTagToWikitext
(
$node
);
if
(
empty
(
$dataMw
->
body
)
)
{
return
$startTagSrc
;
// We self-closed this already.
}
else
{
if
(
isset
(
$dataMw
->
body
->
html
)
)
{
$src
=
$extApi
->
htmlToWikitext
(
[
'extName'
=>
$dataMw
->
name
],
$dataMw
->
body
->
html
);
return
$startTagSrc
.
$src
.
'</'
.
$dataMw
->
name
.
'>'
;
}
else
{
$extApi
->
log
(
'error'
,
'References body unavailable for: '
.
DOMCompat
::
getOuterHTML
(
$node
)
);
return
''
;
// Drop it!
}
}
}
}
/** @inheritDoc */
public
function
lintHandler
(
ParsoidExtensionAPI
$extApi
,
Element
$refs
,
callable
$defaultHandler
):
bool
{
$dataMw
=
DOMDataUtils
::
getDataMw
(
$refs
);
if
(
isset
(
$dataMw
->
body
->
html
)
)
{
$fragment
=
$extApi
->
htmlToDom
(
$dataMw
->
body
->
html
);
$defaultHandler
(
$fragment
);
}
return
true
;
}
/** @inheritDoc */
public
function
diffHandler
(
ParsoidExtensionAPI
$extApi
,
callable
$domDiff
,
Element
$origNode
,
Element
$editedNode
):
bool
{
$origDataMw
=
DOMDataUtils
::
getDataMw
(
$origNode
);
$editedDataMw
=
DOMDataUtils
::
getDataMw
(
$editedNode
);
if
(
isset
(
$origDataMw
->
body
->
html
)
&&
isset
(
$editedDataMw
->
body
->
html
)
)
{
$origFragment
=
$extApi
->
htmlToDom
(
$origDataMw
->
body
->
html
,
$origNode
->
ownerDocument
,
[
'markNew'
=>
true
]
);
$editedFragment
=
$extApi
->
htmlToDom
(
$editedDataMw
->
body
->
html
,
$editedNode
->
ownerDocument
,
[
'markNew'
=>
true
]
);
return
call_user_func
(
$domDiff
,
$origFragment
,
$editedFragment
);
}
// FIXME: Similar to DOMDiff::subtreeDiffers, maybe $editNode should
// be marked as inserted to avoid losing any edits, at the cost of
// more normalization
return
false
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 05:42 (9 h, 51 m ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
fb/5c/d951741ff7dd41cfa4838d38659c
Default Alt Text
References.php (29 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment