Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4123660
PageHTMLParser.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
9 KB
Referenced Files
None
Subscribers
None
PageHTMLParser.js
View Options
const
Thumbnail
=
require
(
'./Thumbnail'
),
HEADING_SELECTOR
=
mw
.
config
.
get
(
'wgMFMobileFormatterHeadings'
,
[
'h1'
,
'h2'
,
'h3'
,
'h4'
,
'h5'
]
).
join
(
','
),
EXCLUDE_THUMBNAIL_CLASS_SELECTORS
=
[
'noviewer'
,
'metadata'
],
NOT_SELECTORS
=
EXCLUDE_THUMBNAIL_CLASS_SELECTORS
.
map
(
(
excludeSelector
)
=>
`:not(.
${
excludeSelector
}
)`
).
join
(
''
),
THUMB_SELECTOR
=
[
'a.image'
,
'a.thumbimage, a.mw-file-description'
].
map
(
(
selector
)
=>
`
${
selector
}${
NOT_SELECTORS
}
`
).
join
(
','
);
class
PageHTMLParser
{
/**
* @constructor
* @class module:mobile.startup/PageHTMLParser
* @classdesc Parses an article and converts it into a queriable object.
* @param {jQuery.Object} $container Used when parsing to find children within
* this container
*/
constructor
(
$container
)
{
this
.
$el
=
$container
;
// T220751: Cache headings as $el.find is a very expensive call.
/** @private */
this
.
$headings
=
this
.
$el
.
find
(
HEADING_SELECTOR
);
}
/**
* Find the heading in the page.
* This has the benefit of excluding any additional h2s and h3s that may
* have been added programatically.
*
* @memberof module:mobile.startup/PageHTMLParser
* @param {number} sectionIndex as defined by the PHP parser.
* It should correspond to the section id
* used in the edit link for the section.
* Note, confusingly, this is different from section "ID" which is
* used in methods
* @return {jQuery.Object}
*/
findSectionHeadingByIndex
(
sectionIndex
)
{
if
(
sectionIndex
<
1
)
{
// negative indexes will search from the end, which is behaviour we do not want.
// return an empty set when this happens.
// eslint-disable-next-line no-undef
return
$
(
[]
);
}
else
{
return
this
.
$headings
// Headings must strictly be a child element of a section element
// or the parser-output.
// Not an ancestor!
.
filter
(
'.mw-parser-output > *, [class^="mf-section-"] > *'
).
eq
(
sectionIndex
-
1
);
}
}
/**
* Finds all child elements that match the selector in a given section or subsection.
* Returns any direct child elements that match the selector,
* (i.e. searches only one level deep)
* as well as any elements that match the selector within those children.
* If the Page has no headings (e.g. a stub),
* then the search will target all nodes within the page.
*
* This code should work on desktop (PHP parser HTML)
* as well as mobile formatted HTML (PHP parser + MobileFormatter)
*
* @memberof module:mobile.startup/PageHTMLParser
* @param {number} sectionIndex as defined by the PHP parser. It should correspond to
* the section id used in the edit link for the section.
* Note, confusingly, this is different from section "ID" which is
* used in methods
* @param {string} selector to match
* @return {jQuery.Object}
*/
findChildInSectionLead
(
sectionIndex
,
selector
)
{
let
$heading
,
$nextHeading
;
const
headingSelector
=
HEADING_SELECTOR
;
function
withNestedChildren
(
$matchingNodes
)
{
return
$matchingNodes
.
find
(
selector
).
addBack
();
}
if
(
sectionIndex
===
0
)
{
// lead is easy
const
$lead
=
this
.
getLeadSectionElement
();
if
(
$lead
&&
$lead
.
length
)
{
// Handle nested sections in Parsoid wikitext parset opt-in scenario.
const
$nestedSection
=
$lead
.
find
(
'section[data-mw-section-id="0"]'
);
if
(
$nestedSection
.
length
)
{
return
withNestedChildren
(
$nestedSection
.
children
(
selector
)
);
}
return
withNestedChildren
(
$lead
.
children
(
selector
)
);
}
else
{
$heading
=
this
.
findSectionHeadingByIndex
(
1
);
return
$heading
.
length
?
withNestedChildren
(
$heading
.
prevAll
(
selector
)
)
:
// this page is a stub so search entire page
this
.
$el
.
find
(
selector
);
}
}
// find heading associated with the section by looking at its
// index position in the article
// section ids relate to the element position in the page and the first heading
// lead has been dealt with above, so first heading corresponds to section 1,
// the first heading in the article.
$heading
=
this
.
findSectionHeadingByIndex
(
sectionIndex
);
// If section-heading is present on the heading,
// then we know the page has been MobileFormatted
// and that this is a wrapped section
if
(
$heading
.
hasClass
(
'section-heading'
)
)
{
// get content of section
const
$el
=
$heading
.
next
();
// inside section find the first heading
$nextHeading
=
$el
.
find
(
headingSelector
).
eq
(
0
);
return
$nextHeading
.
length
?
// find all amboxes before the next heading
withNestedChildren
(
$nextHeading
.
prevAll
(
selector
)
)
:
// There is no subheadings inside
// Grab all issues in section
withNestedChildren
(
$el
.
children
(
selector
)
);
}
else
{
// the heading relates to a subsection (or unwrapped desktop section),
// so grab elements between this and the next one
$nextHeading
=
$heading
.
eq
(
0
).
nextAll
(
headingSelector
).
eq
(
0
);
return
$heading
.
nextUntil
(
$nextHeading
,
selector
);
}
}
/**
* Get the lead section of the page view.
*
* @memberof module:mobile.startup/PageHTMLParser
* @return {jQuery.Object|null}
*/
getLeadSectionElement
()
{
/*
* The page is formatted as follows:
* <div id="bodyContent">
* <!-- content of the page.. -->
* <div id="mw-content-text">
* <div class="mf-section-0">lead section</div>
* <h2></h2>
* <div class="mf-section-1">second section</div>
* </div>
* </div>
*/
const
$leadSection
=
this
.
$el
.
find
(
'.mf-section-0'
);
if
(
$leadSection
.
length
)
{
return
$leadSection
;
}
// no lead section found
return
null
;
}
/**
* Returns a Thumbnail object from an anchor element containing an image or
* null if not valid.
*
* @memberof module:mobile.startup/PageHTMLParser
* @param {jQuery} $a Anchor element that contains the image.
* @return {Thumbnail|null}
*/
getThumbnail
(
$a
)
{
const
notSelector
=
'.'
+
EXCLUDE_THUMBNAIL_CLASS_SELECTORS
.
join
(
',.'
),
$lazyImage
=
$a
.
find
(
'.lazy-image-placeholder'
),
href
=
$a
.
attr
(
'href'
),
url
=
href
&&
new
URL
(
href
,
location
.
href
),
legacyTitle
=
url
&&
url
.
searchParams
.
get
(
'title'
),
match
=
url
&&
url
.
pathname
.
match
(
/[^/]+$/
);
// Parents need to be checked as well.
let
valid
=
$a
.
parents
(
notSelector
).
length
===
0
&&
$a
.
find
(
notSelector
).
length
===
0
;
// filter out invalid lazy loaded images if so far image is valid
if
(
$lazyImage
.
length
&&
valid
)
{
// if the regex matches it means the image has one of the classes
// thus we must invert the result
valid
=
!
new
RegExp
(
'\\b('
+
EXCLUDE_THUMBNAIL_CLASS_SELECTORS
.
join
(
'|'
)
+
')\\b'
)
.
test
(
$lazyImage
.
data
(
'class'
)
);
}
if
(
valid
&&
(
legacyTitle
!==
null
||
match
)
)
{
return
new
Thumbnail
(
{
el
:
$a
,
filename
:
mw
.
util
.
percentDecodeFragment
(
legacyTitle
!==
null
?
legacyTitle
:
match
[
0
]
)
}
);
}
return
null
;
}
/**
* Return all the thumbnails in the article.
* Images which have a class or link container (.image|.thumbimage)
* that matches one of the items of the constant EXCLUDE_THUMBNAIL_CLASS_SELECTORS
* will be excluded.
* A thumbnail nested inside one of these classes will still be returned.
* e.g. `<div class="noviewer"><a class="image"><img></a></div>` is not a valid thumbnail
* `<a class="image noviewer"><img></a>` is not a valid thumbnail
* `<a class="image"><img class="noviewer"></a>` is not a valid thumbnail
*
* @memberof module:mobile.startup/PageHTMLParser
* @param {jQuery} [$el] Container to search, defaults to this.$el.
* @return {Thumbnail[]}
*/
getThumbnails
(
$el
)
{
const
self
=
this
,
thumbs
=
[];
$el
=
$el
||
this
.
$el
;
const
$thumbs
=
$el
.
find
(
THUMB_SELECTOR
);
$thumbs
.
each
(
function
()
{
const
$a
=
$el
.
find
(
this
);
const
thumb
=
self
.
getThumbnail
(
$a
);
if
(
thumb
)
{
thumbs
.
push
(
thumb
);
}
}
);
return
thumbs
;
}
/**
* Returns a jQuery object representing all redlinks on the page.
*
* @memberof module:mobile.startup/PageHTMLParser
* @return {jQuery.Object}
*/
getRedLinks
()
{
return
this
.
$el
.
find
(
'.new'
);
}
/**
* Returns an object consistent with MediaWiki API representing languages
* associated with the page in the user's current language.
*
* @memberof module:mobile.startup/PageHTMLParser
* @param {string} pageTitle to fallback to if none found
* @return {Object} containing langlinks
* and variant links as defined @ https://en.m.wikipedia.org/w/api.php?action=help&modules=query%2Blanglinks
*/
getLanguages
(
pageTitle
)
{
const
mapLinkToLanguageObj
=
(
node
)
=>
{
const
DELIMITER
=
' – '
;
// Name of language (e.g. עברית for Hebrew)
const
autonym
=
node
.
textContent
;
// The name of the language in the current language
// e.g. for english this would be Hebrew
let
langname
;
let
title
=
node
.
getAttribute
(
'title'
)
||
pageTitle
;
if
(
title
.
indexOf
(
DELIMITER
)
>
-
1
)
{
title
=
title
.
split
(
DELIMITER
);
langname
=
title
.
pop
();
title
=
title
.
join
(
DELIMITER
);
}
if
(
!
langname
)
{
langname
=
autonym
;
}
return
{
lang
:
node
.
getAttribute
(
'hreflang'
),
autonym
,
langname
,
title
,
url
:
node
.
getAttribute
(
'href'
)
};
};
return
{
languages
:
Array
.
prototype
.
map
.
call
(
document
.
querySelectorAll
(
'#p-lang .interlanguage-link a'
),
mapLinkToLanguageObj
),
variants
:
Array
.
prototype
.
map
.
call
(
document
.
querySelectorAll
(
'#p-variants li a'
),
mapLinkToLanguageObj
)
};
}
}
/**
* Selector for matching headings
*
* @memberof PageHTMLParser
*/
PageHTMLParser
.
HEADING_SELECTOR
=
HEADING_SELECTOR
;
/**
* Selector for thumbnails.
*
* @memberof PageHTMLParser
*/
PageHTMLParser
.
THUMB_SELECTOR
=
THUMB_SELECTOR
;
module
.
exports
=
PageHTMLParser
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Aug 19, 06:17 (5 d, 9 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
3d/f8/83f2b15e8c8106b3edff12677d01
Default Alt Text
PageHTMLParser.js (9 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment