Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4139746
CitizenComponentBodyContent.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
5 KB
Referenced Files
None
Subscribers
None
CitizenComponentBodyContent.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
MediaWiki\Skins\Citizen\Components
;
use
DOMDocument
;
use
DOMElement
;
use
DOMNode
;
use
DOMXpath
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
/**
* CitizenComponentBodyContent component
*/
class
CitizenComponentBodyContent
implements
CitizenComponent
{
/**
* The code below is largely based on the extension MobileFrontend
* All credits go to the author and contributors of the project
*/
/**
* Class name for section wrappers
*/
public
const
SECTION_CLASS
=
'citizen-section'
;
/**
* List of tags that could be considered as section headers.
*/
private
array
$topHeadingTags
=
[
'h1'
,
'h2'
,
'h3'
,
'h4'
,
'h5'
,
'h6'
];
/**
* The tag name of the top-level heading elements used for section breaks.
* This is determined on the fly when processing the content.
*/
private
?
string
$topHeadingName
=
null
;
public
function
__construct
(
private
string
$html
,
private
bool
$shouldMakeSections
)
{
}
/**
* Splits the body of the document into sections in a single pass.
*/
private
function
makeSections
(
DOMDocument
$doc
):
DOMDocument
{
$xpath
=
new
DOMXpath
(
$doc
);
$containers
=
$xpath
->
query
(
'//div[contains(concat(" ",normalize-space(@class)," ")," mw-parser-output ")][1]'
);
if
(
$containers
->
length
===
0
)
{
return
$doc
;
}
$container
=
$containers
->
item
(
0
);
if
(
!(
$container
instanceof
DOMElement
)
)
{
return
$doc
;
}
// Reset state for this run
$this
->
topHeadingName
=
null
;
$sectionNumber
=
0
;
$sectionBody
=
$this
->
createSectionBodyElement
(
$doc
,
$sectionNumber
);
$currentNode
=
$container
->
firstChild
;
while
(
$currentNode
)
{
$nextNode
=
$currentNode
->
nextSibling
;
if
(
!
$this
->
isSectionBreak
(
$currentNode
)
)
{
$sectionBody
->
appendChild
(
$currentNode
);
}
else
{
$container
->
insertBefore
(
$sectionBody
,
$currentNode
);
$this
->
prepareHeading
(
$doc
,
$currentNode
);
$sectionNumber
++;
$sectionBody
=
$this
->
createSectionBodyElement
(
$doc
,
$sectionNumber
);
}
$currentNode
=
$nextNode
;
}
// Append the final section body, which contains all nodes after the last heading.
$container
->
appendChild
(
$sectionBody
);
return
$doc
;
}
/**
* Determines if a given node should be treated as a section break.
* This method has the side effect of setting the `$topHeadingName`
* property when the first valid section heading is found.
*/
private
function
isSectionBreak
(
DOMNode
$node
):
bool
{
if
(
!
$node
instanceof
DOMElement
)
{
return
false
;
}
$currentHeadingName
=
$this
->
getHeadingName
(
$node
);
if
(
!
$currentHeadingName
)
{
return
false
;
}
if
(
$this
->
topHeadingName
===
null
)
{
// This is the first potential heading. If it's valid, make it the standard.
if
(
$this
->
isValidSectionHeading
(
$node
)
)
{
$this
->
topHeadingName
=
$currentHeadingName
;
return
true
;
}
return
false
;
}
else
{
// This is a subsequent heading. Check if it matches the top-level one.
return
$currentHeadingName
===
$this
->
topHeadingName
;
}
}
private
function
getHeadingName
(
DOMNode
$node
):
?
string
{
if
(
!(
$node
instanceof
DOMElement
)
)
{
return
null
;
}
// We accept both kinds of nodes: a `<h1>` to `<h6>` node, or a
// `<div class="mw-heading">` node wrapping it. In the future, the wrapper
// will be required (T13555).
if
(
DOMCompat
::
getClassList
(
$node
)->
contains
(
'mw-heading'
)
)
{
$headingNode
=
DOMCompat
::
querySelector
(
$node
,
implode
(
','
,
$this
->
topHeadingTags
)
);
if
(
$headingNode
instanceof
DOMElement
)
{
$tagName
=
$headingNode
->
tagName
;
return
in_array
(
$tagName
,
$this
->
topHeadingTags
)
?
$tagName
:
null
;
}
return
null
;
}
$tagName
=
$node
->
tagName
;
return
in_array
(
$tagName
,
$this
->
topHeadingTags
)
?
$tagName
:
null
;
}
/**
* Checks if a node is a valid heading for creating a section.
* This is used to filter out headings that shouldn't create sections,
* e.g., headings inside the Table of Contents.
*/
private
function
isValidSectionHeading
(
DOMElement
$element
):
bool
{
// A heading element can be the element itself (h1-h6) or a wrapper div.
$headingElement
=
$element
;
if
(
!
in_array
(
$element
->
tagName
,
$this
->
topHeadingTags
)
)
{
// If the element is not a heading tag, it might be a wrapper.
$found
=
DOMCompat
::
querySelector
(
$element
,
implode
(
','
,
$this
->
topHeadingTags
)
);
if
(
!
$found
)
{
// Not a valid heading wrapper.
return
false
;
}
}
$parent
=
$headingElement
->
parentNode
;
if
(
!(
$parent
instanceof
DOMElement
)
)
{
// Should not happen in a valid document.
return
false
;
}
$classList
=
DOMCompat
::
getClassList
(
$parent
);
// Ignore headings inside the Table of Contents.
return
!
$classList
->
contains
(
'toctitle'
);
}
/**
* Prepare section headings, add required classes
*/
private
function
prepareHeading
(
DOMDocument
$doc
,
DOMElement
$heading
):
void
{
DOMCompat
::
getClassList
(
$heading
)->
add
(
'citizen-section-heading'
);
// prepend indicator - this avoids a reflow by creating a placeholder for a toggling indicator
$indicator
=
$doc
->
createElement
(
'span'
);
$indicator
->
setAttribute
(
'class'
,
'citizen-section-indicator citizen-ui-icon mw-ui-icon-wikimedia-collapse'
);
$heading
->
insertBefore
(
$indicator
,
$heading
->
firstChild
);
}
/**
* Creates a Section body element
*/
private
function
createSectionBodyElement
(
DOMDocument
$doc
,
int
$sectionNumber
):
DOMElement
{
$sectionBody
=
$doc
->
createElement
(
'section'
);
$sectionBody
->
setAttribute
(
'id'
,
'citizen-section-'
.
$sectionNumber
);
$sectionBody
->
setAttribute
(
'class'
,
self
::
SECTION_CLASS
);
return
$sectionBody
;
}
/**
* @inheritDoc
*/
public
function
getTemplateData
():
array
{
$html
=
$this
->
html
;
if
(
$this
->
shouldMakeSections
)
{
$doc
=
DOMUtils
::
parseHTML
(
$html
);
$this
->
makeSections
(
$doc
);
$html
=
DOMCompat
::
getInnerHTML
(
DOMCompat
::
getBody
(
$doc
)
);
}
return
[
'html-body-content'
=>
$html
,
];
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 15:12 (3 w, 5 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
7f/57/114703911f0471986a58df04d8da
Default Alt Text
CitizenComponentBodyContent.php (5 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment