footnotes = []; * $this->footnoteLinkNum = 0; * $this->footnoteLinks = []; * } * ``` * * Make sure to add parsed footnotes on postprocess(): * * ```php * protected function prepare() * { * 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() === '') ? '' : $this->getContextId() . '-'; $hr = $this->html5 ? "
\n" : "
\n"; $footnotesHtml = "
\n$hr
    \n"; foreach ($footnotesSorted as $footnoteInfo) { $backLinks = []; foreach ($footnoteInfo['refs'] as $refIndex => $refNum) { $fnref = count($footnoteInfo['refs']) > 1 ? $footnoteInfo['num'] . '-' . $refIndex : $footnoteInfo['num']; $backLinks[] = ''. "\u{21A9}\u{FE0E}" . ''; } $linksPara = '

    ' . join("\n", $backLinks) . '

    '; $footnotesHtml .= "
  1. \n" // Footnotes might themselves contain footnote links. . $this->numberFootnotes( $footnoteInfo['html'], $footnotesSorted ) . $linksPara . "\n
  2. \n"; } $footnotesHtml .= "
\n
\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() === '') ? '' : $this->getContextId() . '-'; $objChr = "\u{FFFC}"; $substituteRefnum = $objChr . "footnote-refnum" . $block['num'] . $objChr; $substituteNum = $objChr . "footnote-num" . $block['num'] . $objChr; return '' . '' . $substituteNum . '' . ''; } /** * 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); }