Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4133244
MakeSectionsTransform.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
MakeSectionsTransform.php
View Options
<?php
namespace
MobileFrontend\Transforms
;
use
DOMDocument
;
use
DOMElement
;
use
DOMNode
;
use
DOMXPath
;
use
LogicException
;
use
MediaWiki\Html\Html
;
use
MediaWiki\ResourceLoader\ResourceLoader
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
/**
* Implements IMobileTransform, that splits the body of the document into
* sections demarcated by the $headings elements. Also moves the first paragraph
* in the lead section above the infobox.
*
* All member elements of the sections are added to a <code><div></code> so
* that the section bodies are clearly defined (to be "expandable" for
* example).
*
* @see IMobileTransform
*/
class
MakeSectionsTransform
implements
IMobileTransform
{
/**
* Class name for collapsible section wrappers
*/
public
const
STYLE_COLLAPSIBLE_SECTION_CLASS
=
'collapsible-block'
;
/**
* Whether scripts can be added in the output.
* @var bool
*/
private
$scriptsEnabled
;
/**
* List of tags that could be considered as section headers.
* @var array
*/
private
$topHeadingTags
;
/**
*
* @param array $topHeadingTags list of tags could ne cosidered as sections
* @param bool $scriptsEnabled wheather scripts are enabled
*/
public
function
__construct
(
array
$topHeadingTags
,
bool
$scriptsEnabled
)
{
$this
->
topHeadingTags
=
$topHeadingTags
;
$this
->
scriptsEnabled
=
$scriptsEnabled
;
}
/**
* @param DOMNode|null $node
* @return string|false Heading tag name if the node is a heading
*/
private
function
getHeadingName
(
$node
)
{
if
(
!(
$node
instanceof
DOMElement
)
)
{
return
false
;
}
// We accept both kinds of nodes that can be returned by getTopHeadings():
// a `<h1>` to `<h6>` node, or a `<div class="mw-heading">` node wrapping it.
// In the future `<div class="mw-heading">` will be required (T13555).
if
(
DOMCompat
::
getClassList
(
$node
)->
contains
(
'mw-heading'
)
)
{
$node
=
DOMCompat
::
querySelector
(
$node
,
implode
(
','
,
$this
->
topHeadingTags
)
);
if
(
!(
$node
instanceof
DOMElement
)
)
{
return
false
;
}
}
return
$node
->
tagName
;
}
/**
* Actually splits the body of the document into sections
*
* @param DOMElement $body representing the HTML of the current article. In the HTML the sections
* should not be wrapped.
* @param DOMElement[] $headingWrappers The headings (or wrappers) returned by getTopHeadings():
* `<h1>` to `<h6>` nodes, or `<div class="mw-heading">` nodes wrapping them.
* In the future `<div class="mw-heading">` will be required (T13555).
*/
private
function
makeSections
(
DOMElement
$body
,
array
$headingWrappers
)
{
$ownerDocument
=
$body
->
ownerDocument
;
if
(
$ownerDocument
===
null
)
{
return
;
}
// Find the parser output wrapper div
$xpath
=
new
DOMXPath
(
$ownerDocument
);
$containers
=
$xpath
->
query
(
// Equivalent of CSS attribute `~=` to support multiple classes
'body/div[contains(concat(" ",normalize-space(@class)," ")," mw-parser-output ")][1]'
);
if
(
!
$containers
->
length
)
{
// No wrapper? This could be an old parser cache entry, or perhaps the
// OutputPage contained something that was not generated by the parser.
// Try using the <body> as the container.
$containers
=
$xpath
->
query
(
'body'
);
if
(
!
$containers
->
length
)
{
throw
new
LogicException
(
"HTML lacked body element even though we put it there ourselves"
);
}
}
$container
=
$containers
->
item
(
0
);
$containerChild
=
$container
->
firstChild
;
$firstHeading
=
reset
(
$headingWrappers
);
$firstHeadingName
=
$this
->
getHeadingName
(
$firstHeading
);
$sectionNumber
=
0
;
$sectionBody
=
$this
->
createSectionBodyElement
(
$ownerDocument
,
$sectionNumber
,
false
);
while
(
$containerChild
)
{
$node
=
$containerChild
;
$containerChild
=
$containerChild
->
nextSibling
;
// If we've found a top level heading, insert the previous section if
// necessary and clear the container div.
if
(
$firstHeadingName
&&
$this
->
getHeadingName
(
$node
)
===
$firstHeadingName
)
{
// The heading we are transforming is always 1 section ahead of the
// section we are currently processing
/** @phan-suppress-next-line PhanTypeMismatchArgumentSuperType DOMNode vs. DOMElement */
$this
->
prepareHeading
(
$ownerDocument
,
$node
,
$sectionNumber
+
1
,
$this
->
scriptsEnabled
);
// Insert the previous section body and reset it for the new section
$container
->
insertBefore
(
$sectionBody
,
$node
);
$sectionNumber
+=
1
;
$sectionBody
=
$this
->
createSectionBodyElement
(
$ownerDocument
,
$sectionNumber
,
$this
->
scriptsEnabled
);
continue
;
}
// If it is not a top level heading, keep appending the nodes to the
// section body container.
$sectionBody
->
appendChild
(
$node
);
}
// Append the last section body.
$container
->
appendChild
(
$sectionBody
);
}
/**
* Prepare section headings, add required classes and onclick actions
*
* @param DOMDocument $doc
* @param DOMElement $heading
* @param int $sectionNumber
* @param bool $isCollapsible
*/
private
function
prepareHeading
(
DOMDocument
$doc
,
DOMElement
$heading
,
$sectionNumber
,
$isCollapsible
)
{
$className
=
$heading
->
hasAttribute
(
'class'
)
?
$heading
->
getAttribute
(
'class'
)
.
' '
:
''
;
$heading
->
setAttribute
(
'class'
,
$className
.
'section-heading'
);
if
(
$isCollapsible
)
{
$heading
->
setAttribute
(
'onclick'
,
'mfTempOpenSection('
.
$sectionNumber
.
')'
);
}
// prepend indicator - this avoids a reflow by creating a placeholder for a toggling indicator
$indicator
=
$doc
->
createElement
(
'span'
);
$indicator
->
setAttribute
(
'class'
,
'indicator mf-icon mf-icon-expand mf-icon--small'
);
$heading
->
insertBefore
(
$indicator
,
$heading
->
firstChild
??
$heading
);
}
/**
* Creates a Section body element
*
* @param DOMDocument $doc
* @param int $sectionNumber
* @param bool $isCollapsible
*
* @return DOMElement
*/
private
function
createSectionBodyElement
(
DOMDocument
$doc
,
$sectionNumber
,
$isCollapsible
)
{
$sectionClass
=
'mf-section-'
.
$sectionNumber
;
if
(
$isCollapsible
)
{
// TODO: Probably good to rename this to the more generic 'section'.
// We have no idea how the skin will use this.
$sectionClass
.=
' '
.
self
::
STYLE_COLLAPSIBLE_SECTION_CLASS
;
}
// FIXME: The class `/mf\-section\-[0-9]+/` is kept for caching reasons
// but given class is unique usage is discouraged. [T126825]
$sectionBody
=
$doc
->
createElement
(
'section'
);
$sectionBody
->
setAttribute
(
'class'
,
$sectionClass
);
$sectionBody
->
setAttribute
(
'id'
,
'mf-section-'
.
$sectionNumber
);
return
$sectionBody
;
}
/**
* Gets top headings in the document.
*
* Note well that the rank order is defined by the
* <code>MobileFormatter#topHeadingTags</code> property.
*
* @param DOMElement $doc
* @return array An array first is the highest rank headings
*/
private
function
getTopHeadings
(
DOMElement
$doc
):
array
{
$headings
=
[];
foreach
(
$this
->
topHeadingTags
as
$tagName
)
{
$allTags
=
DOMCompat
::
querySelectorAll
(
$doc
,
$tagName
);
foreach
(
$allTags
as
$el
)
{
$parent
=
$el
->
parentNode
;
if
(
!(
$parent
instanceof
DOMElement
)
)
{
continue
;
}
// Use the `<div class="mw-heading">` wrapper if it is present. When they are required
// (T13555), the querySelectorAll() above can use the class and this can be removed.
if
(
DOMCompat
::
getClassList
(
$parent
)->
contains
(
'mw-heading'
)
)
{
$el
=
$parent
;
}
// This check can be removed too when we require the wrappers.
if
(
$parent
->
getAttribute
(
'class'
)
!==
'toctitle'
)
{
$headings
[]
=
$el
;
}
}
if
(
$headings
)
{
return
$headings
;
}
}
return
$headings
;
}
/**
* Make it possible to open sections while JavaScript is still loading.
*
* @return string The JavaScript code to add event handlers to the skin
*/
public
static
function
interimTogglingSupport
()
{
$js
=
<<<JAVASCRIPT
function mfTempOpenSection( id ) {
var block = document.getElementById( "mf-section-" + id );
block.className += " open-block";
// The previous sibling to the content block is guaranteed to be the
// associated heading due to mobileformatter. We need to add the same
// class to flip the collapse arrow icon.
// <h[1-6]>heading</h[1-6]><div id="mf-section-[1-9]+"></div>
block.previousSibling.className += " open-block";
}
JAVASCRIPT;
return
Html
::
inlineScript
(
ResourceLoader
::
filter
(
'minify-js'
,
$js
)
);
}
/**
* Performs html transformation that splits the body of the document into
* sections demarcated by the $headings elements. Also moves the first paragraph
* in the lead section above the infobox.
* @param DOMElement $doc html document
*/
public
function
apply
(
DOMElement
$doc
)
{
$this
->
makeSections
(
$doc
,
$this
->
getTopHeadings
(
$doc
)
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 12:03 (3 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
4a/d0/a0fb26cc050550aee0a6286f46a1
Default Alt Text
MakeSectionsTransform.php (8 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment