Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4110981
ContentHolder.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
10 KB
Referenced Files
None
Subscribers
None
ContentHolder.php
View Options
<?php
declare
(
strict_types
=
1
);
/**
* @license GPL-2.0-or-later
* @file
*/
namespace
MediaWiki\Parser
;
use
MediaWiki\MediaWikiServices
;
use
Wikimedia\Assert\Assert
;
use
Wikimedia\Parsoid\Core\BasePageBundle
;
use
Wikimedia\Parsoid\Core\DomPageBundle
;
use
Wikimedia\Parsoid\Core\HtmlPageBundle
;
use
Wikimedia\Parsoid\DOM\Document
;
use
Wikimedia\Parsoid\DOM\DocumentFragment
;
use
Wikimedia\Parsoid\Utils\ContentUtils
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMDataUtils
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
/**
* @internal
* @unstable
* @since 1.45
*
* A ContentHolder holds a map of fragments that can be HTML string or DOM fragments.
* It should, as much as possible, be used in a consistent format and/or limit format switches, as conversions of
* all the fragments happen on all accessors, which has a performance impact.
*
* ContentHolder currently makes no guarantee on the preservation of the document outside of the <body> tag. In
* particular, if a full Parsoid document with a <head> tag is passed as a string, and converted to DOM, the
* <head> content is lost. This must be taken into account in particular if we create an ExtractBody DOM pass, in
* which case the <base> tag contained in the <head> must be handled before conversion. OutputTransform steps
* after ExtractBody should however be unaffected.
*/
class
ContentHolder
{
public
const
BODY_FRAGMENT
=
"body"
;
private
function
__construct
(
private
Document
$ownerDocument
,
private
?
BasePageBundle
$pageBundle
=
null
,
/**
* Contains the string representation of the fragments.
* $htmlMap[BODY_FRAGMENT] might contain the full document
* @var array<string,string>
*/
private
array
$htmlMap
=
[],
/**
* $domMap[BODY_FRAGMENT] does not contain the <body> tag
* @var array<string,DocumentFragment>
*/
private
array
$domMap
=
[],
private
bool
$isParsoidContent
=
false
,
private
bool
$domFormat
=
false
)
{
}
/**
* Create a ContentHolder from a legacy body HTML string, typically
* returned by the legacy parser.
*/
public
static
function
createFromLegacyString
(
string
$html
):
ContentHolder
{
$ch
=
new
ContentHolder
(
ownerDocument
:
DOMCompat
::
newDocument
(),
htmlMap
:
[
self
::
BODY_FRAGMENT
=>
$html
],
isParsoidContent
:
false
,
);
return
$ch
;
}
/**
* Create a ContentHolder from a Parsoid HtmlPageBundle.
*/
public
static
function
createFromParsoidPageBundle
(
HtmlPageBundle
$pb
):
ContentHolder
{
$htmlMap
=
[
self
::
BODY_FRAGMENT
=>
$pb
->
html
,
]
+
$pb
->
fragments
;
$ch
=
new
ContentHolder
(
ownerDocument
:
ContentUtils
::
createAndLoadDocument
(
''
),
pageBundle
:
$pb
->
toBasePageBundle
(),
htmlMap
:
$htmlMap
,
isParsoidContent
:
true
,
);
return
$ch
;
}
/**
* Creates an empty ContentHolder that can be used as a placeholder.
*
* This does not contain any body content.
*/
public
static
function
createEmpty
():
ContentHolder
{
return
new
ContentHolder
(
ownerDocument
:
DOMCompat
::
newDocument
(),
isParsoidContent
:
false
,
);
}
/**
* Returns true if this ContentHolder contains Parsoid-generated
* content.
*/
public
function
isParsoidContent
():
bool
{
// Right now, this invariant feels worth keeping because it helps to make sure that we're doing what we
// think we're doing; this can however be revisited if we decide to use parts of the pageBundle for legacy
// content as well.
Assert
::
invariant
(
$this
->
isParsoidContent
===
(
$this
->
pageBundle
!==
null
),
'Inconsistency between parsoid status and bundle existence'
);
return
$this
->
isParsoidContent
;
}
/**
* Returns false if the designated fragment is not present in the
* ContentHolder.
*/
public
function
has
(
string
$fragmentName
):
bool
{
return
isset
(
$this
->
htmlMap
[
$fragmentName
]
)
||
isset
(
$this
->
domMap
[
$fragmentName
]
);
}
/**
* Returns the designated fragment as an HTML string version, or null if
* it is not present.
*
* @note If a conversion is needed, at present all fragments of the
* document are converted to HTML strings.
*/
public
function
getAsHtmlString
(
string
$fragmentName
=
self
::
BODY_FRAGMENT
):
?
string
{
if
(
$this
->
domFormat
)
{
$this
->
convertDomToHtml
();
}
return
$this
->
htmlMap
[
$fragmentName
]
??
null
;
}
/**
* Returns the designated fragment as a DOM DocumentFragment, or null if
* it is not present.
*
* @note If a conversion is needed, at present all fragments of the
* document are converted to DOM DocumentFragments.
*/
public
function
getAsDom
(
string
$fragmentName
=
self
::
BODY_FRAGMENT
):
?
DocumentFragment
{
if
(
!
$this
->
domFormat
)
{
$this
->
convertHtmlToDom
();
}
return
$this
->
domMap
[
$fragmentName
]
??
null
;
}
/**
* Sets or removes a fragment, provided as an HTML string.
* @param string $fragmentName name of the fragment to set
* @param string|null $html string of the fragment to set, or null to
* remove a fragment.
* @return void
*
* @note The self::BODY_FRAGMENT should not contain the top-level <body>
* tag.
* @note All fragments may be converted to HTML strings as a side-effect.
*/
public
function
setAsHtmlString
(
string
$fragmentName
=
self
::
BODY_FRAGMENT
,
?
string
$html
=
null
):
void
{
// no need to convert the fragment that we're going to replace
unset
(
$this
->
domMap
[
$fragmentName
]
);
if
(
$html
===
null
)
{
unset
(
$this
->
htmlMap
[
$fragmentName
]
);
return
;
}
if
(
$this
->
domFormat
)
{
$this
->
convertDomToHtml
();
}
if
(
$fragmentName
===
self
::
BODY_FRAGMENT
)
{
Assert
::
invariant
(
!
str_starts_with
(
$html
,
'<body'
),
"Body fragment should not contain a body tag"
);
}
$this
->
htmlMap
[
$fragmentName
]
=
$html
;
}
/**
* Sets or removes a fragment, provided as a DOM DocumentFragment.
* @param string $fragmentName name of the fragment to set
* @param DocumentFragment|null $fragment the fragment to set, or null to
* remove a fragment.
* @return void
*
* @note The self::BODY_FRAGMENT should not contain the top-level <body>
* tag.
* @note All fragments may be converted to DOM DocumentFragments as a
* side-effect.
*/
public
function
setAsDom
(
string
$fragmentName
=
self
::
BODY_FRAGMENT
,
?
DocumentFragment
$fragment
=
null
)
{
// no need to convert the fragment that we're going to replace
unset
(
$this
->
htmlMap
[
$fragmentName
]
);
if
(
$fragment
===
null
)
{
unset
(
$this
->
domMap
[
$fragmentName
]
);
return
;
}
if
(
!
$this
->
domFormat
)
{
$this
->
convertHtmlToDom
();
}
Assert
::
invariant
(
$fragment
->
ownerDocument
===
$this
->
ownerDocument
,
"Fragment not owned by the ContentHolder document."
);
$firstChild
=
$fragment
->
firstElementChild
;
if
(
$fragmentName
===
self
::
BODY_FRAGMENT
&&
$firstChild
)
{
Assert
::
invariant
(
DOMUtils
::
nodeName
(
$firstChild
)
!==
"body"
,
"Body fragment should not contain a body tag"
);
}
$this
->
domMap
[
$fragmentName
]
=
$fragment
;
}
public
function
createFragment
(
?
string
$html
=
null
):
DocumentFragment
{
if
(
!
$this
->
domFormat
)
{
$this
->
convertHtmlToDom
();
}
if
(
$html
===
null
)
{
return
$this
->
ownerDocument
->
createDocumentFragment
();
}
if
(
$this
->
isParsoidContent
()
)
{
return
ContentUtils
::
createAndLoadDocumentFragment
(
$this
->
ownerDocument
,
$html
);
}
return
DOMUtils
::
parseHTMLToFragment
(
$this
->
ownerDocument
,
$html
);
}
public
function
addFragment
(
string
$name
=
self
::
BODY_FRAGMENT
,
?
string
$html
=
null
):
DocumentFragment
{
$frag
=
$this
->
createFragment
(
$html
);
$this
->
setAsDom
(
$name
,
$frag
);
return
$frag
;
}
/**
* Return the BasePageBundle of the ContentHolder.
* @internal
*/
public
function
getBasePageBundle
():
BasePageBundle
{
Assert
::
invariant
(
$this
->
isParsoidContent
(),
'getBasePageBundle called on non-Parsoid ContentHolder'
);
if
(
$this
->
domFormat
)
{
// Ensure that data-parsoid and data-mw are serialized into
// the page bundle.
$this
->
convertDomToHtml
();
}
return
$this
->
pageBundle
;
}
private
function
convertHtmlToDom
()
{
if
(
$this
->
domFormat
)
{
return
;
}
if
(
$this
->
isParsoidContent
()
)
{
$hasBody
=
$this
->
has
(
self
::
BODY_FRAGMENT
);
$fragments
=
$this
->
htmlMap
;
$html
=
$fragments
[
self
::
BODY_FRAGMENT
]
??
''
;
unset
(
$fragments
[
self
::
BODY_FRAGMENT
]
);
$dpb
=
DomPageBundle
::
fromHtmlPageBundle
(
$this
->
pageBundle
->
withHtml
(
$html
,
$fragments
)
);
$this
->
ownerDocument
=
$dpb
->
toDom
();
$this
->
domMap
=
$dpb
->
fragments
;
if
(
$hasBody
)
{
$frag
=
$this
->
ownerDocument
->
createDocumentFragment
();
DOMUtils
::
migrateChildren
(
DOMCompat
::
getBody
(
$this
->
ownerDocument
),
$frag
);
$this
->
domMap
[
self
::
BODY_FRAGMENT
]
=
$frag
;
}
}
else
{
foreach
(
$this
->
htmlMap
as
$name
=>
$html
)
{
$this
->
domMap
[
$name
]
=
DOMUtils
::
parseHTMLToFragment
(
$this
->
ownerDocument
,
$html
);
}
}
$this
->
htmlMap
=
[];
$this
->
domFormat
=
true
;
}
private
function
convertDomToHtml
()
{
if
(
!
$this
->
domFormat
)
{
return
;
}
if
(
$this
->
isParsoidContent
()
)
{
$siteConfig
=
MediaWikiServices
::
getInstance
()->
getParsoidSiteConfig
();
$body
=
$this
->
domMap
[
self
::
BODY_FRAGMENT
]
??
null
;
unset
(
$this
->
domMap
[
self
::
BODY_FRAGMENT
]
);
if
(
$body
!==
null
&&
$body
->
hasChildNodes
()
)
{
DOMCompat
::
getBody
(
$this
->
ownerDocument
)->
appendChild
(
$body
);
}
$pb
=
HtmlPageBundle
::
fromDomPageBundle
(
DomPageBundle
::
fromLoadedDocument
(
$this
->
ownerDocument
,
[
'pageBundle'
=>
$this
->
pageBundle
,
],
$this
->
domMap
,
$siteConfig
),
[
'body_only'
=>
true
,
]
);
$this
->
pageBundle
=
$pb
->
toBasePageBundle
();
$this
->
htmlMap
=
$pb
->
fragments
;
$this
->
htmlMap
[
self
::
BODY_FRAGMENT
]
=
$pb
->
html
;
}
else
{
foreach
(
$this
->
domMap
as
$name
=>
$df
)
{
$this
->
htmlMap
[
$name
]
=
ContentUtils
::
toXML
(
$df
,
[
'innerXML'
=>
true
]
);
}
}
$this
->
domMap
=
[];
$this
->
domFormat
=
false
;
}
public
function
__clone
()
{
$this
->
ownerDocument
=
DOMDataUtils
::
cloneDocument
(
$this
->
ownerDocument
);
foreach
(
$this
->
domMap
as
$name
=>
&
$fragment
)
{
$fragment
=
$this
->
ownerDocument
->
importNode
(
$fragment
,
true
);
}
if
(
$this
->
pageBundle
)
{
$this
->
pageBundle
=
clone
$this
->
pageBundle
;
}
}
public
function
ignoreForObjectEquality
():
array
{
return
[
"ownerDocument"
];
}
/**
* Given a ContentHolderTransformStage that has two valid transform options, returns true if the state of the
* ContentHolder calls for a DOM transform, false if it calls for a text transform.
* Right now, this is strictly directed by whether the ContentHolder is in DOM format or in HTML format;
* this might change in the future if we maintain both formats in some cases.
*/
public
function
preferDom
():
bool
{
return
$this
->
domFormat
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Tue, Aug 18, 22:06 (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d0/e9/ca3afca35fa672c52ce04025db54
Default Alt Text
ContentHolder.php (10 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment