Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4138423
Parsoid.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
26 KB
Referenced Files
None
Subscribers
None
Parsoid.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
Wikimedia\Parsoid
;
use
Composer\InstalledVersions
;
use
Composer\Semver\Comparator
;
use
Composer\Semver\Semver
;
use
InvalidArgumentException
;
use
LogicException
;
use
Wikimedia\Bcp47Code\Bcp47Code
;
use
Wikimedia\Parsoid\Config\DataAccess
;
use
Wikimedia\Parsoid\Config\Env
;
use
Wikimedia\Parsoid\Config\PageConfig
;
use
Wikimedia\Parsoid\Config\SiteConfig
;
use
Wikimedia\Parsoid\Config\StubMetadataCollector
;
use
Wikimedia\Parsoid\Core\ContentMetadataCollector
;
use
Wikimedia\Parsoid\Core\PageBundle
;
use
Wikimedia\Parsoid\Core\ResourceLimitExceededException
;
use
Wikimedia\Parsoid\Core\SelectiveUpdateData
;
use
Wikimedia\Parsoid\DOM\Document
;
use
Wikimedia\Parsoid\Ext\ParsoidExtensionAPI
;
use
Wikimedia\Parsoid\Language\LanguageConverter
;
use
Wikimedia\Parsoid\Logger\LintLogger
;
use
Wikimedia\Parsoid\Utils\ComputeSelectiveStats
;
use
Wikimedia\Parsoid\Utils\ContentUtils
;
use
Wikimedia\Parsoid\Utils\DOMCompat
;
use
Wikimedia\Parsoid\Utils\DOMDataUtils
;
use
Wikimedia\Parsoid\Utils\DOMUtils
;
use
Wikimedia\Parsoid\Utils\Timing
;
use
Wikimedia\Parsoid\Utils\Utils
;
use
Wikimedia\Parsoid\Wikitext\Wikitext
;
use
Wikimedia\Parsoid\Wt2Html\DOM\Processors\AddRedLinks
;
use
Wikimedia\Parsoid\Wt2Html\DOM\Processors\ConvertOffsets
;
class
Parsoid
{
/**
* Available HTML content versions.
* @see https://www.mediawiki.org/wiki/Parsoid/API#Content_Negotiation
* @see https://www.mediawiki.org/wiki/Specs/HTML#Versioning
*/
public
const
AVAILABLE_VERSIONS
=
[
'2.8.0'
,
'999.0.0'
];
private
const
DOWNGRADES
=
[
[
'from'
=>
'999.0.0'
,
'to'
=>
'2.0.0'
,
'func'
=>
'downgrade999to2'
],
];
/** @var SiteConfig */
private
$siteConfig
;
/** @var DataAccess */
private
$dataAccess
;
public
function
__construct
(
SiteConfig
$siteConfig
,
DataAccess
$dataAccess
)
{
$this
->
siteConfig
=
$siteConfig
;
$this
->
dataAccess
=
$dataAccess
;
}
/**
* Returns the currently-installed version of Parsoid.
* @return string
*/
public
static
function
version
():
string
{
try
{
// See https://getcomposer.org/doc/07-runtime.md#knowing-the-version-of-package-x
return
InstalledVersions
::
getVersion
(
'wikimedia/parsoid'
)
??
// From the composer runtime API docs:
// "It is nonetheless a good idea to make sure you
// handle the null return value as gracefully as
// possible for safety."
'null'
;
}
catch
(
\Throwable
$t
)
{
// Belt-and-suspenders protection against parts of the composer
// runtime API being absent in production.
return
'error'
;
}
}
/**
* Returns the default HTML content version
* @return string
*/
public
static
function
defaultHTMLVersion
():
string
{
return
self
::
AVAILABLE_VERSIONS
[
0
];
}
/**
* See if any content version Parsoid knows how to produce satisfies the
* the supplied version, when interpreted with semver caret semantics.
* This will allow us to make backwards compatible changes, without the need
* for clients to bump the version in their headers all the time.
*
* @param string $version
* @return string|null
*/
public
static
function
resolveContentVersion
(
string
$version
)
{
foreach
(
self
::
AVAILABLE_VERSIONS
as
$i
=>
$a
)
{
if
(
Semver
::
satisfies
(
$a
,
"^{$version}"
)
&&
// The section wrapping in 1.6.x should have induced a major
// version bump, since it requires upgrading clients to
// handle it. We therefore hardcode this in so that we can
// fail hard.
Comparator
::
greaterThanOrEqualTo
(
$version
,
'1.6.0'
)
)
{
return
$a
;
}
}
return
null
;
}
/**
* Determine if language conversion is enabled, aka if the optional
* wikimedia/langconv library is installed.
* @return bool True if the wikimedia/langconv library is available
*/
public
static
function
supportsLanguageConversion
():
bool
{
return
class_exists
(
'
\W
ikimedia
\L
angConv
\R
eplacementMachine'
);
}
private
function
setupCommonOptions
(
array
$options
):
array
{
$envOptions
=
[];
if
(
isset
(
$options
[
'offsetType'
]
)
)
{
$envOptions
[
'offsetType'
]
=
$options
[
'offsetType'
];
}
if
(
isset
(
$options
[
'traceFlags'
]
)
)
{
$envOptions
[
'traceFlags'
]
=
$options
[
'traceFlags'
];
}
if
(
isset
(
$options
[
'dumpFlags'
]
)
)
{
$envOptions
[
'dumpFlags'
]
=
$options
[
'dumpFlags'
];
}
if
(
isset
(
$options
[
'debugFlags'
]
)
)
{
$envOptions
[
'debugFlags'
]
=
$options
[
'debugFlags'
];
}
if
(
!
empty
(
$options
[
'htmlVariantLanguage'
]
)
)
{
$envOptions
[
'htmlVariantLanguage'
]
=
$options
[
'htmlVariantLanguage'
];
}
if
(
!
empty
(
$options
[
'wtVariantLanguage'
]
)
)
{
$envOptions
[
'wtVariantLanguage'
]
=
$options
[
'wtVariantLanguage'
];
}
if
(
isset
(
$options
[
'logLevels'
]
)
)
{
$envOptions
[
'logLevels'
]
=
$options
[
'logLevels'
];
}
return
$envOptions
;
}
/**
* Parsing code shared between the next two methods.
*
* @param PageConfig $pageConfig
* @param ContentMetadataCollector $metadata
* @param array $options See wikitext2html.
* @param ?SelectiveUpdateData $selparData See wikitext2html.
* @return array{0:Env,1:Document,2:?string}
*/
private
function
parseWikitext
(
PageConfig
$pageConfig
,
ContentMetadataCollector
$metadata
,
array
$options
=
[],
?
SelectiveUpdateData
$selparData
=
null
):
array
{
$envOptions
=
$this
->
setupCommonOptions
(
$options
);
if
(
isset
(
$options
[
'outputContentVersion'
]
)
)
{
$envOptions
[
'outputContentVersion'
]
=
$options
[
'outputContentVersion'
];
}
$envOptions
[
'discardDataParsoid'
]
=
!
empty
(
$options
[
'discardDataParsoid'
]
);
if
(
isset
(
$options
[
'wrapSections'
]
)
)
{
$envOptions
[
'wrapSections'
]
=
(
bool
)
$options
[
'wrapSections'
];
}
if
(
isset
(
$options
[
'pageBundle'
]
)
)
{
$envOptions
[
'pageBundle'
]
=
(
bool
)
$options
[
'pageBundle'
];
}
if
(
isset
(
$options
[
'logLinterData'
]
)
)
{
$envOptions
[
'logLinterData'
]
=
(
bool
)
$options
[
'logLinterData'
];
}
if
(
isset
(
$options
[
'linterOverrides'
]
)
)
{
$envOptions
[
'linterOverrides'
]
=
$options
[
'linterOverrides'
];
}
$envOptions
[
'skipLanguageConversionPass'
]
=
$options
[
'skipLanguageConversionPass'
]
??
false
;
$env
=
new
Env
(
$this
->
siteConfig
,
$pageConfig
,
$this
->
dataAccess
,
$metadata
,
$envOptions
);
if
(
!
$env
->
compareWt2HtmlLimit
(
'wikitextSize'
,
strlen
(
$env
->
topFrame
->
getSrcText
()
)
)
)
{
throw
new
ResourceLimitExceededException
(
"wt2html: wikitextSize limit exceeded"
);
}
$contentmodel
=
$options
[
'contentmodel'
]
??
null
;
$handler
=
$env
->
getContentHandler
(
$contentmodel
);
$extApi
=
new
ParsoidExtensionAPI
(
$env
);
// FIXME: Hardcoded to assume 'mode' is 'template'
return
[
$env
,
$handler
->
toDOM
(
$extApi
,
$selparData
),
$contentmodel
];
}
/**
* Parse the wikitext supplied in a `PageConfig` to HTML.
*
* @param PageConfig $pageConfig
* @param array $options [
* 'wrapSections' => (bool) Whether `<section>` wrappers should be added.
* 'pageBundle' => (bool) Sets ids on nodes and stores
* data-* attributes in a JSON blob.
* 'body_only' => (bool|null) Only return the <body> children (T181657)
* 'outputContentVersion' => (string|null) Version of HTML to output.
* `null` returns the default version.
* 'contentmodel' => (string|null) The content model of the input.
* 'discardDataParsoid' => (bool) Drop all data-parsoid annotations.
* 'offsetType' => (string) ucs2, char, byte are valid values
* what kind of source offsets should be emitted?
* 'skipLanguageConversionPass' => (bool) Skip the language variant conversion pass (defaults to false)
* 'htmlVariantLanguage' => (Bcp47Code) If non-null, the language variant used for Parsoid HTML.
* 'wtVariantLanguage' => (Bcp47Code) If non-null, the language variant used for wikitext.
* 'logLinterData' => (bool) Should we log linter data if linting is enabled?
* 'linterOverrides' => (array) Override the site linting configs.
* // Debugging options, not for use in production
* 'traceFlags' => (array) associative array with tracing options
* 'dumpFlags' => (array) associative array with dump options
* 'debugFlags' => (array) associative array with debug options
* 'logLevels' => (string[]) Levels to log
* // Experimental options, not considered stable
* 'sampleStats' => (bool) If true, okay to perform "expensive"
* analysis to generate metrics.
* 'renderReason' => (?string) Passed through from MediaWiki core
* to classify metrics; see
* ParserOptions::getRenderReason()
* 'previousInput' => (?PageConfig) wikitext, revision ID, etc of
* some recent parse of this page.
* Not guaranteed to be usable for selective
* update, and could even be from a "newer"
* revision (if this is a render of an old
* revision).
* 'previousOutput' => (?PageBundle) output of the prior parse of
* 'previousInput'
* ]
* @param ?array &$headers
* @param ?ContentMetadataCollector $metadata Pass in a CMC in order to
* collect and retrieve metadata about the parse.
* @param ?SelectiveUpdateData $selparData
* @return PageBundle|string
*/
public
function
wikitext2html
(
PageConfig
$pageConfig
,
array
$options
=
[],
?
array
&
$headers
=
null
,
?
ContentMetadataCollector
$metadata
=
null
,
?
SelectiveUpdateData
$selparData
=
null
)
{
if
(
$metadata
===
null
)
{
$metadata
=
new
StubMetadataCollector
(
$this
->
siteConfig
);
}
$parseTiming
=
Timing
::
start
();
[
$env
,
$doc
,
$contentmodel
]
=
$this
->
parseWikitext
(
$pageConfig
,
$metadata
,
$options
,
$selparData
);
$parseTimeMs
=
$parseTiming
->
end
();
// FIXME: Does this belong in parseWikitext so that the other endpoint
// is covered as well? It probably depends on expectations of the
// Rest API. If callers of /page/lint/ assume that will update the
// results on the Special page.
if
(
$env
->
linting
()
)
{
(
new
LintLogger
(
$env
)
)->
logLintOutput
();
}
$headers
=
DOMUtils
::
findHttpEquivHeaders
(
$doc
);
$body_only
=
!
empty
(
$options
[
'body_only'
]
);
$node
=
$body_only
?
DOMCompat
::
getBody
(
$doc
)
:
$doc
;
if
(
$env
->
pageBundle
)
{
$out
=
ContentUtils
::
extractDpAndSerialize
(
$node
,
[
'innerXML'
=>
$body_only
,
]
);
}
else
{
$out
=
[
'html'
=>
ContentUtils
::
toXML
(
$node
,
[
'innerXML'
=>
$body_only
,
]
),
];
}
$this
->
recordParseMetrics
(
$env
,
$parseTimeMs
,
$out
,
$headers
,
$contentmodel
,
$options
);
if
(
$env
->
pageBundle
)
{
return
new
PageBundle
(
$out
[
'html'
],
$out
[
'pb'
]->
parsoid
,
$out
[
'pb'
]->
mw
??
null
,
$env
->
getOutputContentVersion
(),
$headers
,
$contentmodel
);
}
else
{
return
$out
[
'html'
];
}
}
/**
*
*/
private
function
recordParseMetrics
(
Env
$env
,
float
$parseTimeMs
,
array
$out
,
?
array
$headers
,
string
$contentmodel
,
array
$options
)
{
$metrics
=
$this
->
siteConfig
->
metrics
();
$pageConfig
=
$env
->
getPageConfig
();
// This is somewhat suspect because ParsoidHandler::tryToCreatePageConfig
// can set a revision id on a MutableRevisionRecord, but it might be simpler
// to make that go away
if
(
$pageConfig
->
getRevisionId
()
)
{
$mstr
=
'pageWithOldid'
;
}
else
{
$mstr
=
'wt'
;
}
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
$parseTimeMs
);
$timing
->
end
(
"entry.wt2html.{$mstr}.parse"
,
'wt2html_parse_seconds'
,
[
'type'
=>
$mstr
]
);
$version
=
'default'
;
if
(
Semver
::
satisfies
(
$env
->
getOutputContentVersion
(),
'!='
.
self
::
defaultHTMLVersion
()
)
)
{
if
(
$metrics
)
{
$metrics
->
increment
(
'entry.wt2html.parse.version.notdefault'
);
}
$version
=
'non-default'
;
}
$this
->
siteConfig
->
incrementCounter
(
'wt2html_parse_total'
,
[
'type'
=>
$mstr
,
'version'
=>
$version
]
);
// @phan-suppress-next-line PhanDeprecatedFunction
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
strlen
(
$pageConfig
->
getPageMainContent
()
)
);
$timing
->
end
(
"entry.wt2html.{$mstr}.size.input"
,
"wt2html_size_input_bytes"
,
[
"type"
=>
$mstr
]
);
$outSize
=
strlen
(
$out
[
'html'
]
);
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
$outSize
);
$timing
->
end
(
"entry.wt2html.{$mstr}.size.output"
,
"wt2html_size_output_bytes"
,
[
"type"
=>
$mstr
]
);
if
(
$parseTimeMs
>
10
&&
$outSize
>
100
)
{
// * Don't bother with this metric for really small parse times
// p99 for initialization time is ~7ms according to grafana.
// So, 10ms ensures that startup overheads don't skew the metrics
// * For body_only=false requests, <head> section isn't generated
// and if the output is small, per-request overheads can skew
// the timePerKB metrics.
//
// NOTE: This is slightly misleading since there are fixed costs
// for generating output like the <head> section and should be factored in,
// but this is good enough for now as a useful first degree of approxmation.
$msPerKB
=
$parseTimeMs
*
1024
/
$outSize
;
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
$msPerKB
);
$timing
->
end
(
'entry.wt2html.timePerKB'
,
'wt2html_msPerKB'
,
[]
);
}
// Expensive analyses: sampleStats is randomly sampled will not be
// true "often"
$doSample
=
$options
[
'sampleStats'
]
??
false
;
if
(
!
$doSample
)
{
return
;
}
try
{
// create new page bundle for this computation to ensure we
// don't inadvertently corrupt the main document result.
$newPb
=
new
PageBundle
(
$out
[
'html'
],
$out
[
'pb'
]->
parsoid
,
$out
[
'pb'
]->
mw
??
null
,
$env
->
getOutputContentVersion
(),
$headers
,
$contentmodel
);
$labels
=
ComputeSelectiveStats
::
classify
(
$env
,
$options
[
'previousInput'
]
??
null
,
$options
[
'previousOutput'
]
??
null
,
$pageConfig
,
$newPb
);
$labels
[
'wiki'
]
=
$this
->
siteConfig
->
iwp
();
$labels
[
'reason'
]
=
$options
[
'renderReason'
]
??
'unknown'
;
$this
->
siteConfig
->
incrementCounter
(
'selective_update_total'
,
$labels
);
$this
->
siteConfig
->
incrementCounter
(
'selective_update_seconds'
,
$labels
,
$parseTimeMs
/
1000.
);
}
catch
(
\Throwable
$t
)
{
// Don't ever allow bugs in the classification code to
// impact the availability of content for read views/editing,
// just log.
$env
->
log
(
'warn'
,
'Classification failure'
,
$t
->
getTraceAsString
()
);
}
}
/**
* Lint the wikitext supplied in a `PageConfig`.
*
* @param PageConfig $pageConfig
* @param array $options See wikitext2html.
* @param ?ContentMetadataCollector $metadata Pass in a CMC in order to
* collect and retrieve metadata about the parse.
* @return array
*/
public
function
wikitext2lint
(
PageConfig
$pageConfig
,
array
$options
=
[],
?
ContentMetadataCollector
$metadata
=
null
):
array
{
if
(
$metadata
===
null
)
{
$metadata
=
new
StubMetadataCollector
(
$this
->
siteConfig
);
}
[
$env
,
]
=
$this
->
parseWikitext
(
$pageConfig
,
$metadata
,
$options
);
return
$env
->
getLints
();
}
/**
* Serialize DOM to wikitext.
*
* @param PageConfig $pageConfig
* @param Document $doc Data attributes are expected to have been applied
* already. Loading them will happen once the environment is created.
* @param array $options [
* 'inputContentVersion' => (string) The content version of the input.
* Necessary if it differs from the current default in order to
* account for any serialization differences.
* 'offsetType' => (string) ucs2, char, byte are valid values
* what kind of source offsets are present in the HTML?
* 'contentmodel' => (string|null) The content model of the input.
* 'htmlVariantLanguage' => (Bcp47Code) If non-null, the language variant used for Parsoid HTML.
* 'wtVariantLanguage' => (Bcp47Code) If non-null, the language variant used for wikitext.
* 'traceFlags' => (array) associative array with tracing options
* 'dumpFlags' => (array) associative array with dump options
* 'debugFlags' => (array) associative array with debug options
* 'logLevels' => (string[]) Levels to log
* 'htmlSize' => (int) Size of the HTML that generated $doc
* ]
* @param ?SelectiveUpdateData $selserData
* @return string
*/
public
function
dom2wikitext
(
PageConfig
$pageConfig
,
Document
$doc
,
array
$options
=
[],
?
SelectiveUpdateData
$selserData
=
null
):
string
{
$envOptions
=
$this
->
setupCommonOptions
(
$options
);
if
(
isset
(
$options
[
'inputContentVersion'
]
)
)
{
$envOptions
[
'inputContentVersion'
]
=
$options
[
'inputContentVersion'
];
}
$envOptions
[
'topLevelDoc'
]
=
$doc
;
$metadata
=
new
StubMetadataCollector
(
$this
->
siteConfig
);
$env
=
new
Env
(
$this
->
siteConfig
,
$pageConfig
,
$this
->
dataAccess
,
$metadata
,
$envOptions
);
$env
->
bumpHtml2WtResourceUse
(
'htmlSize'
,
$options
[
'htmlSize'
]
??
0
);
$contentmodel
=
$options
[
'contentmodel'
]
??
null
;
$handler
=
$env
->
getContentHandler
(
$contentmodel
);
$extApi
=
new
ParsoidExtensionAPI
(
$env
);
$serialTiming
=
Timing
::
start
();
$wikitext
=
$handler
->
fromDOM
(
$extApi
,
$selserData
);
$serialTime
=
$serialTiming
->
end
();
$this
->
recordSerializationMetrics
(
$options
,
$serialTime
,
$wikitext
);
return
$wikitext
;
}
/**
*
*/
private
function
recordSerializationMetrics
(
array
$options
,
float
$serialTime
,
string
$wikitext
)
{
$siteConfig
=
$this
->
siteConfig
;
$metrics
=
$siteConfig
->
metrics
();
$htmlSize
=
$options
[
'htmlSize'
]
??
0
;
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
$htmlSize
);
$timing
->
end
(
'entry.html2wt.size.input'
,
'html2wt_size_input_bytes'
);
if
(
isset
(
$options
[
'inputContentVersion'
]
)
)
{
if
(
$metrics
)
{
$metrics
->
increment
(
'entry.html2wt.original.version.'
.
$options
[
'inputContentVersion'
]
);
}
$this
->
siteConfig
->
incrementCounter
(
'html2wt_original_version'
,
[
'input_content_version'
=>
$options
[
'inputContentVersion'
]
]
);
}
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
$serialTime
);
$timing
->
end
(
'entry.html2wt.total'
,
'html2wt_total_seconds'
,
[]
);
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
strlen
(
$wikitext
)
);
$timing
->
end
(
'entry.html2wt.size.output'
,
'html2wt_size_output_bytes'
,
[]
);
if
(
$htmlSize
)
{
// Avoid division by zero
// NOTE: the name timePerInputKB is misleading, since $htmlSize is
// in characters, not bytes.
$msPerKB
=
$serialTime
*
1024
/
$htmlSize
;
$timing
=
Timing
::
fakeTiming
(
$this
->
siteConfig
,
$msPerKB
);
$timing
->
end
(
'entry.html2wt.timePerInputKB'
,
'html2wt_msPerKB'
,
[]
);
}
}
/**
* Serialize HTML to wikitext. Convenience method for dom2wikitext.
*
* @param PageConfig $pageConfig
* @param string $html
* @param array $options
* @param ?SelectiveUpdateData $selserData
* @return string
*/
public
function
html2wikitext
(
PageConfig
$pageConfig
,
string
$html
,
array
$options
=
[],
?
SelectiveUpdateData
$selserData
=
null
):
string
{
$doc
=
DOMUtils
::
parseHTML
(
$html
,
true
);
$options
[
'htmlSize'
]
??=
mb_strlen
(
$html
);
return
$this
->
dom2wikitext
(
$pageConfig
,
$doc
,
$options
,
$selserData
);
}
/**
* Update the supplied PageBundle based on the `$update` type.
*
* 'convertoffsets': Convert offsets between formats (byte, char, ucs2)
* 'redlinks': Refreshes the classes of known, missing, etc. links.
* 'variant': Converts the HTML based on the supplied variant.
*
* Note that these are DOM transforms, and not roundtrips through wikitext.
*
* @param PageConfig $pageConfig
* @param string $update 'redlinks'|'variant'
* @param PageBundle $pb
* @param array $options
* @return PageBundle
*/
public
function
pb2pb
(
PageConfig
$pageConfig
,
string
$update
,
PageBundle
$pb
,
array
$options
=
[]
):
PageBundle
{
$envOptions
=
[
'pageBundle'
=>
true
,
'topLevelDoc'
=>
DOMUtils
::
parseHTML
(
$pb
->
toHtml
(),
true
),
];
$metadata
=
new
StubMetadataCollector
(
$this
->
siteConfig
);
$env
=
new
Env
(
$this
->
siteConfig
,
$pageConfig
,
$this
->
dataAccess
,
$metadata
,
$envOptions
);
$doc
=
$env
->
topLevelDoc
;
DOMDataUtils
::
visitAndLoadDataAttribs
(
DOMCompat
::
getBody
(
$doc
),
[
'markNew'
=>
true
]
);
$dataBagPB
=
DOMDataUtils
::
getPageBundle
(
$doc
);
switch
(
$update
)
{
case
'convertoffsets'
:
ContentUtils
::
convertOffsets
(
$env
,
$doc
,
$options
[
'inputOffsetType'
],
$options
[
'outputOffsetType'
]
);
$dataBagPB
->
parsoid
[
'offsetType'
]
=
$options
[
'outputOffsetType'
];
$dataBagPB
->
parsoid
[
'counter'
]
=
$pb
->
parsoid
[
'counter'
];
break
;
case
'redlinks'
:
ContentUtils
::
convertOffsets
(
$env
,
$doc
,
$env
->
getRequestOffsetType
(),
'byte'
);
(
new
AddRedLinks
()
)->
run
(
$env
,
DOMCompat
::
getBody
(
$doc
)
);
(
new
ConvertOffsets
()
)->
run
(
$env
,
DOMCompat
::
getBody
(
$doc
),
[],
true
);
break
;
case
'variant'
:
ContentUtils
::
convertOffsets
(
$env
,
$doc
,
$env
->
getRequestOffsetType
(),
'byte'
);
// Note that `maybeConvert` could still be a no-op, in case the
// __NOCONTENTCONVERT__ magic word is present, or the htmlVariant
// is a base language code or otherwise invalid.
$hasWtVariant
=
$options
[
'variant'
][
'wikitext'
]
??
// Deprecated name for this option:
$options
[
'variant'
][
'source'
]
??
false
;
LanguageConverter
::
maybeConvert
(
$env
,
$doc
,
Utils
::
mwCodeToBcp47
(
$options
[
'variant'
][
'html'
]
??
// Deprecated name for this option:
$options
[
'variant'
][
'target'
],
// Be strict in what we accept.
true
,
$this
->
siteConfig
->
getLogger
()
),
$hasWtVariant
?
Utils
::
mwCodeToBcp47
(
$options
[
'variant'
][
'wikitext'
]
??
// Deprecated name for this option:
$options
[
'variant'
][
'source'
],
// Be strict in what we accept.
true
,
$this
->
siteConfig
->
getLogger
()
)
:
null
);
// NOTE: Keep this in sync with code in core's LanguageVariantConverter
// Update content-language and vary headers.
DOMUtils
::
addHttpEquivHeaders
(
$doc
,
[
'content-language'
=>
$env
->
htmlContentLanguageBcp47
()->
toBcp47Code
(),
'vary'
=>
$env
->
htmlVary
()
]
);
(
new
ConvertOffsets
()
)->
run
(
$env
,
DOMCompat
::
getBody
(
$doc
),
[],
true
);
break
;
default
:
throw
new
LogicException
(
$update
.
'is an unknown transformation'
);
}
DOMDataUtils
::
visitAndStoreDataAttribs
(
DOMCompat
::
getBody
(
$doc
),
[
'discardDataParsoid'
=>
$env
->
discardDataParsoid
,
'storeInPageBundle'
=>
$env
->
pageBundle
,
'env'
=>
$env
,
]
);
$body_only
=
!
empty
(
$options
[
'body_only'
]
);
$node
=
$body_only
?
DOMCompat
::
getBody
(
$doc
)
:
$doc
;
DOMDataUtils
::
injectPageBundle
(
$doc
,
$dataBagPB
);
$out
=
ContentUtils
::
extractDpAndSerialize
(
$node
,
[
'innerXML'
=>
$body_only
,
]
);
return
new
PageBundle
(
$out
[
'html'
],
$out
[
'pb'
]->
parsoid
,
$out
[
'pb'
]->
mw
??
null
,
// Prefer the passed in version, since this was just a transformation
$pb
->
version
??
$env
->
getOutputContentVersion
(),
DOMUtils
::
findHttpEquivHeaders
(
$doc
),
// Prefer the passed in content model
$pb
->
contentmodel
??
$pageConfig
->
getContentModel
()
);
}
/**
* Perform pre-save transformations with top-level templates subst'd.
*
* @param PageConfig $pageConfig
* @param string $wikitext
* @return string
*/
public
function
substTopLevelTemplates
(
PageConfig
$pageConfig
,
string
$wikitext
):
string
{
$metadata
=
new
StubMetadataCollector
(
$this
->
siteConfig
);
$env
=
new
Env
(
$this
->
siteConfig
,
$pageConfig
,
$this
->
dataAccess
,
$metadata
);
return
Wikitext
::
pst
(
$env
,
$wikitext
,
true
/* $substTLTemplates */
);
}
/**
* Check whether a given content version can be downgraded to the requested
* content version.
*
* @param string $from Current content version
* @param string $to Requested content version
* @return string[]|null The downgrade that will fulfill the request, as
* [ 'from' => <old version>, 'to' => <new version> ], or null if it
* can't be fulfilled.
*/
public
static
function
findDowngrade
(
string
$from
,
string
$to
):
?
array
{
foreach
(
self
::
DOWNGRADES
as
[
'from'
=>
$dgFrom
,
'to'
=>
$dgTo
]
)
{
if
(
Semver
::
satisfies
(
$from
,
"^$dgFrom"
)
&&
Semver
::
satisfies
(
$to
,
"^$dgTo"
)
)
{
// FIXME: Make this a class?
return
[
'from'
=>
$dgFrom
,
'to'
=>
$dgTo
];
}
}
return
null
;
}
/**
* Downgrade a document to an older content version.
*
* @param string[] $dg Value returned by findDowngrade().
* @param PageBundle $pageBundle
*/
public
static
function
downgrade
(
array
$dg
,
PageBundle
$pageBundle
):
void
{
foreach
(
self
::
DOWNGRADES
as
[
'from'
=>
$dgFrom
,
'to'
=>
$dgTo
,
'func'
=>
$dgFunc
]
)
{
if
(
$dg
[
'from'
]
===
$dgFrom
&&
$dg
[
'to'
]
===
$dgTo
)
{
call_user_func
(
[
self
::
class
,
$dgFunc
],
$pageBundle
);
// FIXME: Maybe this resolve should just be part of the $dg
$pageBundle
->
version
=
self
::
resolveContentVersion
(
$dg
[
'to'
]
);
// FIXME: Maybe this should be a helper to avoid the rt
$doc
=
DOMUtils
::
parseHTML
(
$pageBundle
->
html
);
// Match the http-equiv meta to the content-type header
$meta
=
DOMCompat
::
querySelector
(
$doc
,
'meta[property="mw:htmlVersion"], meta[property="mw:html:version"]'
);
if
(
$meta
)
{
$meta
->
setAttribute
(
'content'
,
$pageBundle
->
version
);
$pageBundle
->
html
=
ContentUtils
::
toXML
(
$doc
);
}
return
;
}
}
throw
new
InvalidArgumentException
(
"Unsupported downgrade: {$dg['from']} -> {$dg['to']}"
);
}
/**
* Check if language variant conversion is implemented for a language
*
* @internal FIXME: Remove once Parsoid's language variant work is completed
* @param PageConfig $pageConfig
* @param Bcp47Code $htmlVariant Variant language to check
* @return bool
*/
public
function
implementsLanguageConversionBcp47
(
PageConfig
$pageConfig
,
Bcp47Code
$htmlVariant
):
bool
{
// Hardcode disable zh lang conversion support since Parsoid's
// implementation is incomplete and not performant (T346657).
if
(
$pageConfig
->
getPageLanguageBcp47
()->
toBcp47Code
()
===
'zh'
)
{
return
false
;
}
$metadata
=
new
StubMetadataCollector
(
$this
->
siteConfig
);
$env
=
new
Env
(
$this
->
siteConfig
,
$pageConfig
,
$this
->
dataAccess
,
$metadata
);
return
LanguageConverter
::
implementsLanguageConversionBcp47
(
$env
,
$htmlVariant
);
}
/**
* Downgrade the given document and pagebundle from 999.x to 2.x.
*
* @param PageBundle $pageBundle
*/
private
static
function
downgrade999to2
(
PageBundle
$pageBundle
)
{
// Effectively, skip applying data-parsoid. Note that if we were to
// support a pb2html downgrade, we'd need to apply the full thing,
// but that would create complications where ids would be left behind.
// See the comment in around `DOMDataUtils::applyPageBundle`
$newPageBundle
=
new
PageBundle
(
$pageBundle
->
html
,
[
'ids'
=>
[]
],
$pageBundle
->
mw
);
$pageBundle
->
html
=
$newPageBundle
->
toHtml
();
// Now, modify the pagebundle to the expected form. This is important
// since, at least in the serialization path, the original pb will be
// applied to the modified content and its presence could cause lost
// deletions.
$pageBundle
->
mw
=
[
'ids'
=>
[]
];
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 14:39 (3 w, 4 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
22/cc/d8518ca044dd7ede757464f7d226
Default Alt Text
Parsoid.php (26 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment