Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5991294
References.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
30 KB
Referenced Files
None
Subscribers
None
References.php
View Options
<?php
declare
(
strict_types
=
1
);
// phpcs:disable MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment
namespace
Cite\Parsoid
;
use
Cite\MarkSymbolRenderer
;
use
Cite\Validator
;
use
MediaWiki\Config\Config
;
use
MediaWiki\Html\HtmlHelper
;
use
MediaWiki\MediaWikiServices
;
use
stdClass
;
use
Wikimedia\Message\MessageParam
;
use
Wikimedia\Message\MessageValue
;
use
Wikimedia\Parsoid\Core\DomSourceRange
;
use
Wikimedia\Parsoid\Core\Sanitizer
;
use
Wikimedia\Parsoid\DOM\Document
;
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\ParsoidExtensionAPI
;
use
Wikimedia\Parsoid\Ext\PHPUtils
;
use
Wikimedia\Parsoid\Ext\WTUtils
;
use
Wikimedia\Parsoid\NodeData\DataMw
;
use
Wikimedia\Parsoid\NodeData\DataMwBody
;
use
Wikimedia\Parsoid\NodeData\DataMwError
;
use
Wikimedia\Parsoid\NodeData\DataParsoid
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\RemexHtml\HTMLData
;
use
Wikimedia\RemexHtml\Serializer\SerializerNode
;
/**
* @license GPL-2.0-or-later
*/
class
References
{
private
const
CONFLICT_NONE
=
0
;
/** A conflict in e.g. the data-mw metadata, but not in the visible content */
private
const
CONFLICT_INVISIBLE
=
1
;
/** A conflict in the content that is visible to the user */
private
const
CONFLICT_VISIBLE
=
2
;
private
Config
$mainConfig
;
private
MarkSymbolRenderer
$markSymbolRenderer
;
private
ParsoidValidator
$validator
;
/** @var array<string,array<string,string>>
* @internal Local copy of ref body HTML for conflict detection. Top level
* key is the ref group name, second level key is the ref name
*/
private
array
$conflictCache
=
[];
public
function
__construct
(
Config
$mainConfig
)
{
$this
->
mainConfig
=
$mainConfig
;
$this
->
markSymbolRenderer
=
MediaWikiServices
::
getInstance
()->
getService
(
'Cite.MarkSymbolRenderer'
);
$this
->
validator
=
new
ParsoidValidator
();
}
private
static
function
hasRef
(
ParsoidExtensionAPI
$extApi
,
Node
$node
):
bool
{
$c
=
$node
->
firstChild
;
while
(
$c
)
{
if
(
$c
instanceof
Element
)
{
if
(
WTUtils
::
isSealedFragmentOfType
(
$c
,
'ref'
)
)
{
return
true
;
}
$hasEmbeddedRefs
=
false
;
$extApi
->
processAttributeEmbeddedDom
(
$c
,
function
(
DocumentFragment
$df
)
use
(
$extApi
,
&
$hasEmbeddedRefs
)
{
$hasEmbeddedRefs
=
$hasEmbeddedRefs
||
self
::
hasRef
(
$extApi
,
$df
);
return
true
;
}
);
if
(
$hasEmbeddedRefs
)
{
return
true
;
}
if
(
self
::
hasRef
(
$extApi
,
$c
)
)
{
return
true
;
}
}
$c
=
$c
->
nextSibling
;
}
return
false
;
}
public
function
createEmptyReferenceListFragment
(
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
->
setExtAttrib
(
'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
processNestedRefInRef
(
ParsoidExtensionAPI
$extApi
,
Element
$refFragment
,
ReferencesData
$referencesData
,
bool
$hasDifferingHtml
):
?
string
{
$refFragmentDp
=
DOMDataUtils
::
getDataParsoid
(
$refFragment
);
if
(
!
empty
(
$refFragmentDp
->
empty
)
||
!
self
::
hasRef
(
$extApi
,
$refFragment
)
)
{
return
null
;
}
// This prevents nested list-defined references from erroneously giving "group mismatch"
// errors.
$referencesData
->
incrementRefDepth
();
$this
->
processRefs
(
$extApi
,
$referencesData
,
$refFragment
);
$referencesData
->
decrementRefDepth
();
if
(
$hasDifferingHtml
)
{
// 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 :)
return
$extApi
->
domToHtml
(
$refFragment
,
true
,
true
);
}
return
null
;
}
private
function
extractRefFromNode
(
ParsoidExtensionAPI
$extApi
,
Element
$node
,
ReferencesData
$referencesData
):
void
{
$doc
=
$node
->
ownerDocument
;
$errs
=
[];
// This is data-parsoid from the dom fragment node that's gone through
// DomSourceRange computation and template wrapping.
$nodeDp
=
DOMDataUtils
::
getDataParsoid
(
$node
);
$contentId
=
$nodeDp
->
html
;
// Extract the ref fragment and ensure it's valid
$refFragment
=
$extApi
->
getContentDOM
(
$contentId
)->
firstChild
;
DOMUtils
::
assertElt
(
$refFragment
);
$refFragmentDp
=
DOMDataUtils
::
getDataParsoid
(
$refFragment
);
$refDataMw
=
DOMDataUtils
::
getDataMw
(
$refFragment
);
// Validate attribute keys
$isSubreferenceSupported
=
$this
->
mainConfig
->
get
(
'CiteSubReferencing'
);
$status
=
Validator
::
filterRefArguments
(
$refDataMw
->
getExtAttribs
()
??
[],
$isSubreferenceSupported
);
$arguments
=
$status
->
getValue
();
// Check for missing content, added ?? '' to fix T259676 crasher
// FIXME: See T260082 for a more complete description of cause and deeper fix
$isOnlyWhitespace
=
trim
(
$refDataMw
->
body
->
extsrc
??
''
)
===
''
;
$isEmptyBody
=
!
empty
(
$refFragmentDp
->
empty
)
||
$isOnlyWhitespace
;
// FIXME: Duplication required for isKnown, but the Validator is supposed to do this.
$groupName
=
$arguments
[
'group'
]
??
$referencesData
->
referenceListGroup
()
??
''
;
$validator
=
new
Validator
(
$referencesData
->
referenceListGroup
(),
false
,
false
);
$text
=
!
empty
(
$refFragmentDp
->
selfClose
)
?
null
:
(
$isEmptyBody
?
''
:
'dummy'
);
$status
->
merge
(
$validator
->
validateRef
(
$text
,
$arguments
),
true
);
if
(
$status
->
isOK
()
)
{
$arguments
=
$status
->
getValue
();
}
foreach
(
$status
->
getMessages
()
as
$msg
)
{
// FIXME: This is only temporary, eventually we want all validation results to be used
switch
(
$msg
->
getKey
()
)
{
// TODO: Remove one by one so they are handled by the default instead
// Currently 3 failures
case
'cite_error_ref_no_key'
:
// Currently 22 failures
case
'cite_error_references_missing_key'
:
break
;
default
:
$errs
[]
=
new
DataMwError
(
$msg
->
getKey
(),
array_map
(
static
fn
(
$p
)
=>
$p
instanceof
MessageParam
?
$p
->
getValue
()
:
$p
,
$msg
->
getParams
()
)
);
}
}
// Extract and validate attribute values
$refName
=
(
string
)
$arguments
[
'name'
];
$followName
=
(
string
)
$arguments
[
'follow'
];
$refDir
=
strtolower
(
(
string
)
$arguments
[
'dir'
]
);
$details
=
$arguments
[
'details'
]
??
''
;
$hasBody
=
isset
(
$refDataMw
->
body
);
// Handle 'about' attribute with priority since it's
// only added when the wrapper is a template sibling.
$about
=
DOMCompat
::
getAttribute
(
$node
,
'about'
)
??
DOMCompat
::
getAttribute
(
$refFragment
,
'about'
);
'@phan-var string $about'
;
// assert that $about is non-null
$hasDetails
=
$details
!==
''
&&
$refName
;
$hasValidFollow
=
false
;
// Wrap the attribute 'follow'
if
(
$followName
)
{
$followSpan
=
$this
->
wrapFollower
(
$doc
,
$refFragment
);
$followSpan
->
setAttribute
(
'about'
,
$about
);
$refFragment
->
appendChild
(
$followSpan
);
}
$refGroup
=
$referencesData
->
getOrCreateRefGroup
(
$groupName
);
$ref
=
$refGroup
->
lookupRefByName
(
$refName
);
$conflicts
=
self
::
CONFLICT_NONE
;
// No point in doing conflict detection on <ref /> without body
if
(
$hasBody
)
{
$conflicts
=
$this
->
checkForConflictingContent
(
$extApi
,
$groupName
,
$refName
,
$ref
->
contentId
??
null
,
$refFragment
);
}
// Handle the attributes 'name' and 'follow'
if
(
$refName
)
{
$nameErrorMessage
=
$this
->
validator
->
validateName
(
$refName
,
$refGroup
,
$referencesData
);
if
(
$nameErrorMessage
)
{
$errs
[]
=
$nameErrorMessage
;
}
}
else
{
if
(
$followName
)
{
// Check that the followed ref exists
$followErrorMessage
=
$this
->
validator
->
validateFollow
(
$followName
,
$refGroup
);
if
(
$followErrorMessage
)
{
$errs
[]
=
$followErrorMessage
;
}
else
{
$hasValidFollow
=
true
;
$ref
=
$refGroup
->
lookupRefByName
(
$followName
);
}
}
}
// Split subref and main ref; add main ref as a list-defined reference
if
(
$hasDetails
)
{
$mainRefExists
=
(
bool
)
$ref
;
// Create new, empty main ref
$ref
??=
$referencesData
->
addRef
(
$refGroup
,
$refName
,
$refDir
);
if
(
$hasBody
)
{
if
(
!
$mainRefExists
)
{
// Create a main ref and transfer the tag body to it,
$ref
->
isMainWithDetails
=
true
;
$ref
->
contentId
=
$contentId
;
}
// @phan-suppress-next-line PhanUndeclaredProperty
$refDataMw
->
mainBody
=
ParsoidAnchorFormatter
::
getNoteTextIdentifier
(
$ref
);
// Flag to help reserialize main ref content into the subref when saving.
// @phan-suppress-next-line PhanUndeclaredProperty
$refDataMw
->
isMainRefBodyWithDetails
=
'1'
;
}
// Switch $ref to a newly-created subref
$ref
=
$referencesData
->
addRef
(
$refGroup
,
$refName
,
$refDir
,
$details
);
// Move content to main ref.
$contentId
=
null
;
$ref
->
contentId
=
null
;
// Move details attribute into subref content.
$ref
->
externalFragment
=
$extApi
->
wikitextToDOM
(
$details
,
[
'processInNewFrame'
=>
true
,
'parseOpts'
=>
[
'context'
=>
'inline'
]
],
true
);
// Subref points to the main ref by name.
// FIXME: should have already asserted that refName exists for all details, see T387193
$refDataMw
->
setExtAttrib
(
'name'
,
null
);
// @phan-suppress-next-line PhanUndeclaredProperty
$refDataMw
->
mainRef
=
$refName
;
$refName
=
''
;
}
$refFragmentHtml
=
$this
->
processNestedRefInRef
(
$extApi
,
$refFragment
,
$referencesData
,
$conflicts
!==
self
::
CONFLICT_NONE
)
??
// Fall back to the normal behavior without any nested <ref>
$extApi
->
domToHtml
(
$refFragment
,
true
);
if
(
$this
->
isNestedInSupWithSameGroupAndName
(
$node
,
$groupName
,
$refName
)
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_included_ref'
);
ErrorUtils
::
addErrorsToNode
(
$node
,
$errs
);
}
// Add ref-index linkback
$linkBackSup
=
$doc
->
createElement
(
'sup'
);
$isVisibleNode
=
false
;
if
(
$hasValidFollow
)
{
// Migrate content from the follow to the ref
if
(
$ref
->
contentId
)
{
$refContent
=
$extApi
->
getContentDOM
(
$ref
->
contentId
)->
firstChild
;
DOMUtils
::
migrateChildren
(
$refFragment
,
$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!
$ref
??=
$referencesData
->
addRef
(
$refGroup
,
$refName
,
$refDir
);
// Handle linkbacks
if
(
$referencesData
->
inEmbeddedContent
()
)
{
$ref
->
embeddedNodes
[]
=
$about
;
// While indicator content is embedded throughout the parsing
// pipeline, it gets added back into the page in a post-processing
// step, so consider it visible for the sake of linkbacks.
if
(
$referencesData
->
inIndicatorContext
()
)
{
$ref
->
visibleNodes
++;
$isVisibleNode
=
true
;
}
}
else
{
$ref
->
nodes
[]
=
$linkBackSup
;
$ref
->
visibleNodes
++;
$isVisibleNode
=
true
;
}
}
// Guaranteed from this point on
'@phan-var RefGroupItem $ref'
;
if
(
isset
(
$arguments
[
'dir'
]
)
)
{
$dirError
=
$this
->
validator
->
validateDir
(
$refDir
,
$ref
);
if
(
$dirError
)
{
$errs
[]
=
$dirError
;
}
}
$hasMissingContent
=
$isEmptyBody
&&
!
$ref
->
externalFragment
;
if
(
$hasMissingContent
)
{
if
(
!
$refName
)
{
if
(
!
empty
(
$refFragmentDp
->
selfClose
)
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_ref_no_key'
);
}
}
if
(
!
empty
(
$refFragmentDp
->
selfClose
)
)
{
unset
(
$refDataMw
->
body
);
}
else
{
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat
::
replaceChildren
(
$refFragment
);
$refDataMw
->
body
=
DataMwBody
::
new
(
[
'html'
=>
$refDataMw
->
body
->
extsrc
??
''
,
]
);
}
}
else
{
if
(
$ref
->
contentId
&&
!
$hasValidFollow
)
{
// Empty the <sup> since we've serialized its children and
// removing it below asserts everything has been migrated out
DOMCompat
::
replaceChildren
(
$refFragment
);
}
if
(
$conflicts
!==
self
::
CONFLICT_NONE
)
{
if
(
$refFragmentHtml
!=
''
&&
$conflicts
===
self
::
CONFLICT_VISIBLE
)
{
$errs
[]
=
new
DataMwError
(
'cite_error_references_duplicate_key'
,
[
$refName
]
);
}
$refDataMw
->
body
=
DataMwBody
::
new
(
[
'html'
=>
$refFragmentHtml
,
]
);
}
else
{
$refDataMw
->
body
=
DataMwBody
::
new
(
[
'id'
=>
ParsoidAnchorFormatter
::
getNoteTextIdentifier
(
$ref
),
]
);
}
}
$this
->
addLinkBackAttributes
(
$linkBackSup
,
$isVisibleNode
?
ParsoidAnchorFormatter
::
getBackLinkIdentifier
(
$ref
)
:
null
,
DOMCompat
::
getAttribute
(
$node
,
'typeof'
),
$about
,
$hasValidFollow
);
$this
->
addLinkBackData
(
$linkBackSup
,
$nodeDp
,
DOMUtils
::
hasTypeOf
(
$node
,
'mw:Transclusion'
)
?
DOMDataUtils
::
getDataMw
(
$node
)
:
$refDataMw
);
// FIXME(T214241): Should the errors be added to data-mw if
// $isTplWrapper? Here and other calls to addErrorsToNode.
ErrorUtils
::
addErrorsToNode
(
$linkBackSup
,
$errs
);
// refLink is the link to the citation
$refLink
=
$doc
->
createElement
(
'a'
);
DOMUtils
::
addAttributes
(
$refLink
,
[
'href'
=>
$extApi
->
getPageUri
()
.
'#'
.
Sanitizer
::
escapeIdForLink
(
ParsoidAnchorFormatter
::
getNoteIdentifier
(
$ref
)
)
]
);
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'
);
$openBracket
=
$doc
->
createElement
(
'span'
,
'['
);
$openBracket
->
setAttribute
(
'class'
,
'cite-bracket'
);
$refLinkSpan
->
appendChild
(
$openBracket
);
$refLinkSpan
->
appendChild
(
$doc
->
createTextNode
(
$this
->
markSymbolRenderer
->
makeLabel
(
$ref
->
group
,
$ref
->
numberInGroup
,
$ref
->
subrefIndex
)
)
);
$closeBracket
=
$doc
->
createElement
(
'span'
,
']'
);
$closeBracket
->
setAttribute
(
'class'
,
'cite-bracket'
);
$refLinkSpan
->
appendChild
(
$closeBracket
);
$refLink
->
appendChild
(
$refLinkSpan
);
$linkBackSup
->
appendChild
(
$refLink
);
// Checking if the <ref> is nested in a link
$aParent
=
DOMUtils
::
findAncestorOfName
(
$node
,
'a'
);
if
(
$aParent
)
{
$this
->
insertChildOutsideOfLink
(
$linkBackSup
,
$aParent
);
// 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
(
$linkBackSup
,
$dsrOffset
);
self
::
setMisnested
(
$refLink
,
$dsrOffset
);
self
::
setMisnested
(
$refLinkSpan
,
$dsrOffset
);
$node
->
parentNode
->
removeChild
(
$node
);
}
else
{
// if not, we insert it where we planned in the first place
$node
->
parentNode
->
replaceChild
(
$linkBackSup
,
$node
);
}
// Keep the first content to compare multiple <ref>s with the same name.
if
(
$ref
->
contentId
===
null
&&
!
$hasMissingContent
)
{
$ref
->
contentId
=
$contentId
;
// Use the dir parameter only from the full definition of a named ref tag
$ref
->
dir
=
$refDir
;
}
elseif
(
$contentId
)
{
DOMCompat
::
remove
(
$refFragment
);
$extApi
->
clearContentDOM
(
$contentId
);
}
}
private
function
insertChildOutsideOfLink
(
Element
$linkBackSup
,
Element
$aParent
):
void
{
// 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
(
$linkBackSup
,
$insertionPoint
);
// Mark the two now un-nested elements as a DOM forest that belongs together
$parentAbout
=
DOMCompat
::
getAttribute
(
$aParent
,
'about'
);
if
(
$parentAbout
!==
null
)
{
$linkBackSup
->
setAttribute
(
'about'
,
$parentAbout
);
}
}
private
function
isNestedInSupWithSameGroupAndName
(
Element
$node
,
string
$groupName
,
string
$refName
):
bool
{
// Nothing to compare with
if
(
!
$refName
)
{
return
false
;
}
$supNode
=
DOMUtils
::
findAncestorOfName
(
$node
,
'sup'
);
while
(
$supNode
)
{
$dataMw
=
DOMDataUtils
::
getDataMw
(
$supNode
);
if
(
$dataMw
&&
(
$dataMw
->
getExtAttrib
(
'group'
)
??
''
)
===
$groupName
&&
(
$dataMw
->
getExtAttrib
(
'name'
)
??
''
)
===
$refName
)
{
return
true
;
}
$supNode
=
DOMUtils
::
findAncestorOfName
(
$supNode
,
'sup'
);
}
return
false
;
}
/**
* wrap the content of the follow attribute
* so that there is no ambiguity
* where to find it when round tripping
*/
private
function
wrapFollower
(
Document
$doc
,
Node
$refFragment
):
Element
{
$followSpan
=
$doc
->
createElement
(
'span'
);
DOMUtils
::
addTypeOf
(
$followSpan
,
'mw:Cite/Follow'
);
$followSpan
->
appendChild
(
$doc
->
createTextNode
(
' '
)
);
DOMUtils
::
migrateChildren
(
$refFragment
,
$followSpan
);
return
$followSpan
;
}
/**
* 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
);
}
public
function
insertReferencesIntoDOM
(
ParsoidExtensionAPI
$extApi
,
Element
$refsNode
,
ReferencesData
$refsData
,
bool
$autoGenerated
=
false
):
void
{
$isTemplateWrapper
=
DOMUtils
::
hasTypeOf
(
$refsNode
,
'mw:Transclusion'
);
$nodeDp
=
DOMDataUtils
::
getDataParsoid
(
$refsNode
);
$groupName
=
$nodeDp
->
group
??
''
;
$refGroup
=
$refsData
->
lookupRefGroup
(
$groupName
);
// 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
)
{
foreach
(
$refGroup
->
refs
as
$ref
)
{
$errs
=
[];
// Mark all refs that are named without content
if
(
$ref
->
name
!==
null
&&
$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
)
{
ErrorUtils
::
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
fn
(
Element
$sup
)
=>
$extApi
->
domToHtml
(
$sup
,
false
,
true
)
.
"
\n
"
,
PHPUtils
::
iterable_to_array
(
DOMCompat
::
querySelectorAll
(
$refsNode
,
'sup[typeof~=
\'
mw:Extension/ref
\'
]'
)
)
);
// Any main+details refs should have the main part appear in the
// reference list.
// TODO: clean up the various use cases for this: eg. don't add
// the main ref if it's already present.
if
(
$refGroup
)
{
$doc
=
$refsNode
->
ownerDocument
;
foreach
(
$refGroup
->
refs
as
$ref
)
{
if
(
$ref
->
isMainWithDetails
&&
$ref
->
contentId
)
{
$sup
=
$doc
->
createElement
(
'sup'
);
DOMUtils
::
addAttributes
(
$sup
,
[
'typeof'
=>
'mw:Extension/ref'
,
]
);
$dataMw
=
new
DataMw
(
[
'name'
=>
'ref'
,
'attrs'
=>
(
object
)[
'name'
=>
$ref
->
name
,
'group'
=>
$ref
->
group
,
],
'body'
=>
DataMwBody
::
new
(
[
'id'
=>
ParsoidAnchorFormatter
::
getNoteTextIdentifier
(
$ref
),
]
),
'isMainWithDetails'
=>
'1'
,
]
);
DOMDataUtils
::
setDataMw
(
$sup
,
$dataMw
);
$refFragment
=
$extApi
->
getContentDOM
(
$ref
->
contentId
)->
firstChild
->
firstChild
;
if
(
$refFragment
)
{
DOMCompat
::
setInnerHTML
(
$sup
,
$extApi
->
domToHtml
(
$refFragment
,
false
,
false
)
);
$nestedRefsHTML
[]
=
$extApi
->
domToHtml
(
$sup
,
false
,
false
);
}
}
}
}
if
(
!
$isTemplateWrapper
)
{
$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
;
}
if
(
$nestedRefsHTML
)
{
$dataMw
->
body
=
DataMwBody
::
new
(
[
'html'
=>
"
\n
"
.
implode
(
$nestedRefsHTML
),
]
);
}
elseif
(
!
$autoGenerated
&&
empty
(
$nodeDp
->
selfClose
)
)
{
$dataMw
->
body
=
DataMwBody
::
new
(
[
'html'
=>
''
,
]
);
}
else
{
unset
(
$dataMw
->
body
);
}
unset
(
$nodeDp
->
selfClose
);
}
$hasResponsiveWrapper
=
false
;
// 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
;
$hasResponsiveWrapper
=
true
;
}
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
);
}
if
(
$autoGenerated
&&
$groupName
!==
''
)
{
$errorUtils
=
new
ErrorUtils
(
$extApi
);
$frag
=
$errorUtils
->
renderParsoidError
(
new
MessageValue
(
'cite_error_group_refs_without_references'
,
[
$groupName
]
)
);
$span
=
DOMCompat
::
getFirstElementChild
(
$frag
);
$refsNode
->
parentNode
->
insertBefore
(
$span
,
$refsNode
->
nextSibling
);
$refsNodeAbout
=
DOMCompat
::
getAttribute
(
$refsNode
,
'about'
);
if
(
$refsNodeAbout
)
{
$span
->
setAttribute
(
'about'
,
$refsNodeAbout
);
}
$destNode
=
$hasResponsiveWrapper
?
DOMCompat
::
getParentElement
(
$refsNode
)
:
$refsNode
;
ErrorUtils
::
addErrorsToNode
(
$destNode
,
[
new
DataMwError
(
'cite_error_group_refs_without_references'
,
[
$groupName
]
)
]
);
}
}
// Remove the group from refsData
$refsData
->
removeRefGroup
(
$groupName
);
}
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'
)
)
{
$inReferenceList
=
$refsData
->
inReferenceList
();
if
(
!
$inReferenceList
)
{
$refsData
->
referencesGroup
=
DOMDataUtils
::
getDataParsoid
(
$child
)->
group
??
''
;
}
$refsData
->
pushEmbeddedContentFlag
();
if
(
$child
->
hasChildNodes
()
)
{
$this
->
processRefs
(
$extApi
,
$refsData
,
$child
);
}
$refsData
->
popEmbeddedContentFlag
();
if
(
!
$inReferenceList
)
{
$refsData
->
referencesGroup
=
null
;
$this
->
insertReferencesIntoDOM
(
$extApi
,
$child
,
$refsData
,
false
);
}
}
else
{
if
(
DOMUtils
::
hasTypeOf
(
$child
,
'mw:Extension/indicator'
)
)
{
$refsData
->
pushEmbeddedContentFlag
(
'indicator'
);
}
else
{
$refsData
->
pushEmbeddedContentFlag
();
}
// Look for <ref>s embedded in data attributes
$extApi
->
processAttributeEmbeddedDom
(
$child
,
function
(
DocumentFragment
$df
)
use
(
$extApi
,
$refsData
)
{
$this
->
processRefs
(
$extApi
,
$refsData
,
$df
);
return
true
;
}
);
$refsData
->
popEmbeddedContentFlag
();
if
(
$child
->
hasChildNodes
()
)
{
$this
->
processRefs
(
$extApi
,
$refsData
,
$child
);
}
}
}
$child
=
$nextChild
;
}
}
private
function
addLinkBackData
(
Element
$linkBackSup
,
DataParsoid
$nodeDp
,
?
DataMw
$dataMw
):
void
{
$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
(
$linkBackSup
,
$dataParsoid
);
DOMDataUtils
::
setDataMw
(
$linkBackSup
,
$dataMw
);
}
private
function
addLinkBackAttributes
(
Element
$linkBackSup
,
?
string
$id
,
?
string
$typeof
,
?
string
$about
,
bool
$hasValidFollow
):
void
{
$class
=
'mw-ref reference'
;
if
(
$hasValidFollow
)
{
$class
.=
' mw-ref-follow'
;
}
DOMUtils
::
addAttributes
(
$linkBackSup
,
[
'about'
=>
$about
,
'class'
=>
$class
,
'id'
=>
$hasValidFollow
?
null
:
$id
,
'rel'
=>
'dc:references'
,
'typeof'
=>
$typeof
,
]
);
DOMUtils
::
removeTypeOf
(
$linkBackSup
,
'mw:DOMFragment/sealed/ref'
);
DOMUtils
::
addTypeOf
(
$linkBackSup
,
'mw:Extension/ref'
);
}
/**
* Compare named <ref> content with previous contents.
*
* 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 roundtripping
* for later <ref>s, we have to record HTML inline for all of them.
*
* @return int One of the self::CONFLICT_… constants specifying the type of conflict detected
*/
private
function
checkForConflictingContent
(
ParsoidExtensionAPI
$extApi
,
string
$refGroupName
,
string
$refName
,
?
string
$existingContentId
,
Element
$newRefFragment
):
int
{
if
(
!
$refName
||
!
$existingContentId
)
{
return
self
::
CONFLICT_NONE
;
}
$this
->
conflictCache
[
$refGroupName
][
$refName
]
??=
self
::
normalizeRef
(
$extApi
->
domToHtml
(
$extApi
->
getContentDOM
(
$existingContentId
)->
firstChild
,
true
)
);
$existingHtml
=
$this
->
conflictCache
[
$refGroupName
][
$refName
];
$html
=
$extApi
->
domToHtml
(
$newRefFragment
,
true
);
// Optimized for performance: usually there is no conflict
if
(
$this
->
normalizeRef
(
$html
)
===
$existingHtml
)
{
return
self
::
CONFLICT_NONE
;
}
elseif
(
$this
->
normalizeRef
(
$html
,
true
)
===
$this
->
normalizeRef
(
$existingHtml
,
true
)
)
{
return
self
::
CONFLICT_INVISIBLE
;
}
else
{
return
self
::
CONFLICT_VISIBLE
;
}
}
/**
* This method removes the data-parsoid and about attributes from the HTML string passed in parameters, so
* that it doesn't interfere for the comparison of identical references.
* Remex does not implement the removal of "foreign" attributes, which means that these attributes cannot be
* removed on math and svg elements (T380977), and that trying to do so crashes the rendering.
* To avoid this, we only apply the normalization to nodes in the HTML namespace. This is wider than the
* exact definition of foreign attributes in Remex, but the other integration points of non-foreign content
* would be embedded in foreign content anyway - whose data-parsoid/about attributes would not be stripped
* anyway, so there's no need to process them.
* This means that identical references containing math or svg tags will be detected as being different.
* This is probably a rare enough corner case. If it is not, implementing the handling of foreign attributes
* (as started in Idf30b3afa00743fd78b015ff080cac29e1673f09) is a path to re-consider.
* The extra parameter $checkContent allows to further suppress error messages for references that are not
* technically identical from a round-tripping perspective (hence the need to still consider them distinct) but
* "close enough" to not bother editors with a confusing error message. This is in particular the case for
* references that involve various levels of templating, which means involve different variation of
* data-mw presence/absence.
*/
private
function
normalizeRef
(
string
$s
,
bool
$removeMetadata
=
false
):
string
{
return
HtmlHelper
::
modifyElements
(
$s
,
static
function
(
SerializerNode
$node
)
use
(
$removeMetadata
):
bool
{
return
$node
->
namespace
==
HTMLData
::
NS_HTML
&&
(
isset
(
$node
->
attrs
[
'data-parsoid'
]
)
||
isset
(
$node
->
attrs
[
'about'
]
)
||
(
$removeMetadata
&&
(
isset
(
$node
->
attrs
[
'data-mw'
]
)
||
isset
(
$node
->
attrs
[
'typeof'
]
)
)
)
);
},
static
function
(
SerializerNode
$node
)
use
(
$removeMetadata
):
SerializerNode
{
unset
(
$node
->
attrs
[
'data-parsoid'
]
);
unset
(
$node
->
attrs
[
'about'
]
);
if
(
$removeMetadata
)
{
unset
(
$node
->
attrs
[
'data-mw'
]
);
unset
(
$node
->
attrs
[
'typeof'
]
);
}
return
$node
;
}
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Oct 4, 01:50 (4 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
8b/ca/04ee18bf5e6423be77db9cd76334
Default Alt Text
References.php (30 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment