footnotes = [];
* $this->footnoteLinkNum = 0;
* $this->footnoteLinks = [];
* }
* ```
*
* Make sure to add parsed footnotes on postprocess():
*
* ```php
* protected function postprocess()
* {
* return parent::postprocess(
* $this->addParsedFootnotes($markup)
* );
* }
* ```
*/
trait FootnoteTrait
{
/**
* @var string[][] - Unordered array of footnotes.
*/
protected $footnotes = [];
/**
* @var int - Incrementing counter of the footnote links.
*/
protected $footnoteLinkNum = 0;
/**
* @var string[] - Ordered array of footnote links.
*/
protected $footnoteLinks = [];
/**
* Add footnotes' HTML to the end of parsed HTML.
*
* @param string $html - The HTML output of Markdown::parse().
* @return string
*/
public function addParsedFootnotes($html): string
{
// Unicode "uncertainty sign" will be used for missing references.
$uncertaintyChr = "\u{2BD1}";
// Sort all found footnotes by the order in which they are linked in the text.
$footnotesSorted = [];
$footnoteNum = 0;
foreach ($this->footnoteLinks as $footnotePos => $footnoteLinkName) {
foreach ($this->footnotes as $footnoteName => $footnoteHtml) {
if ($footnoteLinkName === (string)$footnoteName) {
// First time sorting this footnote.
if (!isset($footnotesSorted[$footnoteName])) {
$footnoteNum++;
$footnotesSorted[$footnoteName] = [
'html' => $footnoteHtml,
'num' => $footnoteNum,
'refs' => [1 => $footnotePos],
];
} else {
// Subsequent times sorting this footnote
// (i.e. every time it's referenced).
$footnotesSorted[$footnoteName]['refs'][] = $footnotePos;
}
}
}
}
$html = $this->numberFootnotes(
$html,
$footnotesSorted
);
// Add the footnote HTML to the end of the document.
return $html . $this->getFootnotesHtml($footnotesSorted);
}
/**
* @param mixed[] $footnotesSorted - Array with 'html', 'num', and 'refs' keys.
* @return string
*/
protected function getFootnotesHtml($footnotesSorted): string
{
if (empty($footnotesSorted)) {
return '';
}
$prefix = $this->getContextId();
if ($prefix !== '') {
$prefix .= '-';
}
$hr = $this->html5 ? "
\n" : "
\n";
$footnotesHtml = "\n";
return $footnotesHtml;
}
/**
* @param $html string - The HTML to operate on.
* @param mixed[] $footnotesSorted - Array with 'num' and 'refs' keys.
* @return string
*/
protected function numberFootnotes($html, $footnotesSorted): string
{
// Unicode "uncertainty sign" will be used for missing references.
$uncertaintyChr = "\u{2BD1}";
// Replace all footnote placeholder links with their sorted numbers.
return preg_replace_callback(
"/\u{FFFC}footnote-(refnum|num)(.*?)\u{FFFC}/",
function ($match) use ($footnotesSorted, $uncertaintyChr) {
$footnoteName = $this->footnoteLinks[$match[2]];
if (!isset($footnotesSorted[$footnoteName])) {
// This is a link to a missing footnote.
// Return the uncertainty sign.
return $uncertaintyChr
. ($match[1] === 'refnum' ? '-' . $match[2] : '');
}
if ($match[1] === 'num') {
// Replace only the footnote number.
return $footnotesSorted[$footnoteName]['num'];
}
if (count($footnotesSorted[$footnoteName]['refs']) > 1) {
// For backlinks:
// some have a footnote number and an additional link number.
// If footnote is referenced more than once, add `-n` suffix.
$linkNum = array_search(
$match[2],
$footnotesSorted[$footnoteName]['refs']
);
return $footnotesSorted[$footnoteName]['num']
. '-'
. $linkNum;
} else {
// Otherwise, just the number.
return $footnotesSorted[$footnoteName]['num'];
}
},
$html
);
}
protected function parseFootnoteLinkMarkers()
{
return array('[^');
}
/**
* Parses a footnote link indicated by `[^`.
*
* @marker [^
* @param $text
* @return array
*/
protected function parseFootnoteLink($text): array
{
if (
preg_match('/^\[\^(.+?)(?footnoteLinkNum++;
$this->footnoteLinks[$this->footnoteLinkNum] = $footnoteName;
// To render a footnote link, we only need to know its link-number,
// which will later be turned into its footnote-number (after sorting).
return [
[
'footnoteLink',
'num' => $this->footnoteLinkNum
],
strlen($matches[0])
];
}
return [['text', $text[0]], 1];
}
/**
* @param string[] $block - Array with 'num' key.
* @return string
*/
protected function renderFootnoteLink($block): string
{
$prefix = $this->getContextId();
if ($prefix !== '') {
$prefix .= '-';
}
$objChr = "\u{FFFC}";
$substituteRefnum = $objChr
. "footnote-refnum"
. $block['num']
. $objChr;
$substituteNum = $objChr
. "footnote-num"
. $block['num']
. $objChr;
return '';
}
/**
* Identify a line as the beginning of a footnote block.
*
* @param $line
* @return false|int
*/
protected function identifyFootnoteList($line): bool
{
return preg_match('/^ {0,3}\[\^(.+?)]:/', $line);
}
/**
* Consume lines for a footnote.
*/
protected function consumeFootnoteList($lines, $current): array
{
$footnotes = [];
$parsedFootnotes = [];
$mw = 0;
for ($i = $current, $count = count($lines); $i < $count; $i++) {
$line = $lines[$i];
$startsFootnote = preg_match(
'/^ {0,3}\[\^(.+?)(? $footnoteLines) {
$parsedFootnotes[$footnoteName] = $this->parseBlocks($footnoteLines);
}
return [['footnoteList', 'content' => $parsedFootnotes], $i];
}
/**
* Renders a footnote.
*
* @param array $block
* @return string
*/
protected function renderFootnoteList($block): string
{
foreach ($block['content'] as $footnoteName => $footnote) {
$this->footnotes[$footnoteName] = $this->renderAbsy($footnote);
}
// Render nothing, because all footnote lists will be concatenated
// at the end of the text using the flavor's `postprocess` method.
return '';
}
abstract protected function parseBlocks($lines);
abstract protected function renderAbsy($absy);
}