1
0
mirror of https://github.com/suk-ws/ph-Bookshelf.git synced 2024-12-05 01:16:53 +08:00

使用 composer, markdown 解析器切换为 commonmark。添加脚注&任务表的 bread-card 样式。

- 使用 composer
  - 声明 php8, ext-xml, ext-mbstring
  - 添加了 league/commonmark>=2.3.8
- 删除了 /lib 文件夹 (以及 parsedown 依赖)
- 抽象化 md 解析代码同时 md 解析器换为 commonmark
  - 使用 commonmark 规范
    + gfm - disallowed html
    + attributes, footnote, description list
- 修改 readme
This commit is contained in:
A.C.Sukazyo Eyre 2023-01-24 19:08:15 +08:00
parent 594a695cfb
commit e8fe17b673
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
14 changed files with 157 additions and 2718 deletions

3
.gitignore vendored
View File

@ -4,6 +4,9 @@ debug/
.idea/
# 忽略服务器运行时文件
vendor/
composer.lock
composer.phar
.well-known/
.user.ini

View File

@ -11,17 +11,23 @@
<br/>
### 安装
## 安装
下载/clone此仓库的内容然后拖进 php 站点根目录。
下载/clone此仓库的内容然后拖进 php 站点根目录即可
**要求 php 环境安装了 php-xml 插件**
7.0及以下旧版本可能叫做 php-dom 插件)
(安装方法应该是能 Google 到的)
### web-server 环境要求
对于 Apache即 .htaccess 支持的 php 环境),可以直接运行。
对于 Nginx 或者别的之类的 php 环境,需要转换一下伪静态配置,以 .htaccess 文件内的内容为依据即可。
- 支援 `.htaccess` 的 Webserver
- 如果使用 Apache:
- 启用模块 `rewrite`
- 为网站根目录设置 `AllowOverride All`
- 使用其它 Webserver可以自行查询如何将 .htaccess 规则转换为你所使用的网站配置并写进你的网站配置当中
- PHP 版本 8.0 以上
(旧版可能可以使用,但未经完全测试)
- PHP 模块 `xml` (旧版可能叫做 `dom`)
- PHP 模块 `mbstring`
- composer 工具以安装项目依赖
- 在 php.ini 中设置 `display_errors` 以及 `display_startup_errors``Off` (或者关闭 `E_WARNING` 及以下 log) <small>(这是由于最开始写代码极不上心导致很多地方都会有可能报出 warn输出在屏幕上会导致很糟糕的使用体验)</small>
<br/>
@ -31,8 +37,6 @@
<br/>
### 开源许可
## 开源许可
项目自身MIT License.
LIB: ParseDown : MIT License.
MIT License.

View File

@ -0,0 +1,30 @@
/******************************************************************************
##############################################################################
##### #####
##### Markdown StyleSheet of ui design BreadCard #####
##### extended support #####
##### for footnote[^1] #####
##### #####
##### @author: Sukazyo Workshop #####
##### @version 1.0 #####
##### #####
##############################################################################
******************************************************************************/
.footnote-ref {
display: inline-block;
border-style: solid;
border-width: 0.15em;
padding: 0 0.2em;
transform: scale(0.6) translate(-0.4em, -0.4em);
margin: 0 -0.5em;
font-weight: 900;
}
.footnote::marker {
font-weight: bolder;
}
.footnote .footnote-backref {
font-size: smaller;
}

View File

@ -2,8 +2,16 @@
##############################################################################
##### #####
##### Markdown StyleSheet of ui design BreadCard #####
##### extended support #####
##### for task list - [x] y #####
##### #####
##### @author: Sukazyo Workshop #####
##### @version 1.0 #####
##### #####
##############################################################################
******************************************************************************/
******************************************************************************/
:is(ul, ol):has( > li > input[type="checkbox"]:disabled ) {
list-style-type: none;
padding-inline-start: 0;
}

20
composer.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "suk-ws/ph-bookshelf",
"description": "一个简单简洁(?)的文档书架实现。",
"keywords": ["website", "documentation-tool", "markdown"],
"minimum-stability": "alpha",
"type": "website",
"license": "MIT",
"authors": [
{
"name": "A.C.Sukazyo Eyre",
"email": "email@example.com"
}
],
"require": {
"php": ">=8.0",
"ext-xml": "*",
"ext-mbstring": "*",
"league/commonmark": ">=2.3.8"
}
}

View File

@ -2,6 +2,6 @@
const APP_NAME = "ph-Bookshelf";
const VERSION = "0.3.0.14";
const VERSION = "0.3.0.15";
const CHANNEL = "suk-ws";
const BRANCH = "master";

View File

@ -1,5 +1,6 @@
<?php
require_once "./vendor/autoload.php";
require_once "./src/Data/SiteMeta.php";
require_once "./src/Data/PageMeta.php";
@ -13,7 +14,7 @@ try {
SiteMeta::load();
// 格式化所给链接,并将链接转化为路径字符串数组
$req = $_GET['p'];
$req = array_key_exists('p', $_GET) ? $_GET['p'] : "";
if (strlen($req) > 0 && $req[strlen($req) - 1] === '/')
$tmp = substr($req, 0, -1);
$uri = explode("/", $req, 2);

File diff suppressed because it is too large Load Diff

View File

@ -1,686 +0,0 @@
<?php
#
#
# Parsedown Extra
# https://github.com/erusev/parsedown-extra
#
# (c) Emanuil Rusev
# http://erusev.com
#
# For the full license information, view the LICENSE file that was distributed
# with this source code.
#
#
class ParsedownExtra extends Parsedown
{
# ~
const version = '0.8.0';
# ~
function __construct()
{
if (version_compare(parent::version, '1.7.1') < 0)
{
throw new Exception('ParsedownExtra requires a later version of Parsedown');
}
$this->BlockTypes[':'] []= 'DefinitionList';
$this->BlockTypes['*'] []= 'Abbreviation';
# identify footnote definitions before reference definitions
array_unshift($this->BlockTypes['['], 'Footnote');
# identify footnote markers before before links
array_unshift($this->InlineTypes['['], 'FootnoteMarker');
}
#
# ~
function text($text)
{
$Elements = $this->textElements($text);
# convert to markup
$markup = $this->elements($Elements);
# trim line breaks
$markup = trim($markup, "\n");
# merge consecutive dl elements
$markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
# add footnotes
if (isset($this->DefinitionData['Footnote']))
{
$Element = $this->buildFootnoteElement();
$markup .= "\n" . $this->element($Element);
}
return $markup;
}
#
# Blocks
#
#
# Abbreviation
protected function blockAbbreviation($Line)
{
if (preg_match('/^\*\[(.+?)\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))
{
$this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];
$Block = array(
'hidden' => true,
);
return $Block;
}
}
#
# Footnote
protected function blockFootnote($Line)
{
if (preg_match('/^\[\^(.+?)\]:[ ]?(.*)$/', $Line['text'], $matches))
{
$Block = array(
'label' => $matches[1],
'text' => $matches[2],
'hidden' => true,
);
return $Block;
}
}
protected function blockFootnoteContinue($Line, $Block)
{
if ($Line['text'][0] === '[' and preg_match('/^\[\^(.+?)\]:/', $Line['text']))
{
return;
}
if (isset($Block['interrupted']))
{
if ($Line['indent'] >= 4)
{
$Block['text'] .= "\n\n" . $Line['text'];
return $Block;
}
}
else
{
$Block['text'] .= "\n" . $Line['text'];
return $Block;
}
}
protected function blockFootnoteComplete($Block)
{
$this->DefinitionData['Footnote'][$Block['label']] = array(
'text' => $Block['text'],
'count' => null,
'number' => null,
);
return $Block;
}
#
# Definition List
protected function blockDefinitionList($Line, $Block)
{
if ( ! isset($Block) or $Block['type'] !== 'Paragraph')
{
return;
}
$Element = array(
'name' => 'dl',
'elements' => array(),
);
$terms = explode("\n", $Block['element']['handler']['argument']);
foreach ($terms as $term)
{
$Element['elements'] []= array(
'name' => 'dt',
'handler' => array(
'function' => 'lineElements',
'argument' => $term,
'destination' => 'elements'
),
);
}
$Block['element'] = $Element;
$Block = $this->addDdElement($Line, $Block);
return $Block;
}
protected function blockDefinitionListContinue($Line, array $Block)
{
if ($Line['text'][0] === ':')
{
$Block = $this->addDdElement($Line, $Block);
return $Block;
}
else
{
if (isset($Block['interrupted']) and $Line['indent'] === 0)
{
return;
}
if (isset($Block['interrupted']))
{
$Block['dd']['handler']['function'] = 'textElements';
$Block['dd']['handler']['argument'] .= "\n\n";
$Block['dd']['handler']['destination'] = 'elements';
unset($Block['interrupted']);
}
$text = substr($Line['body'], min($Line['indent'], 4));
$Block['dd']['handler']['argument'] .= "\n" . $text;
return $Block;
}
}
#
# Header
protected function blockHeader($Line)
{
$Block = parent::blockHeader($Line);
if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
{
$attributeString = $matches[1][0];
$Block['element']['attributes'] = $this->parseAttributeData($attributeString);
$Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
}
return $Block;
}
#
# Markup
protected function blockMarkup($Line)
{
if ($this->markupEscaped or $this->safeMode)
{
return;
}
if (preg_match('/^<(\w[\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
{
$element = strtolower($matches[1]);
if (in_array($element, $this->textLevelElements))
{
return;
}
$Block = array(
'name' => $matches[1],
'depth' => 0,
'element' => array(
'rawHtml' => $Line['text'],
'autobreak' => true,
),
);
$length = strlen($matches[0]);
$remainder = substr($Line['text'], $length);
if (trim($remainder) === '')
{
if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
{
$Block['closed'] = true;
$Block['void'] = true;
}
}
else
{
if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
{
return;
}
if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
{
$Block['closed'] = true;
}
}
return $Block;
}
}
protected function blockMarkupContinue($Line, array $Block)
{
if (isset($Block['closed']))
{
return;
}
if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
{
$Block['depth'] ++;
}
if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
{
if ($Block['depth'] > 0)
{
$Block['depth'] --;
}
else
{
$Block['closed'] = true;
}
}
if (isset($Block['interrupted']))
{
$Block['element']['rawHtml'] .= "\n";
unset($Block['interrupted']);
}
$Block['element']['rawHtml'] .= "\n".$Line['body'];
return $Block;
}
protected function blockMarkupComplete($Block)
{
if ( ! isset($Block['void']))
{
$Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']);
}
return $Block;
}
#
# Setext
protected function blockSetextHeader($Line, array $Block = null)
{
$Block = parent::blockSetextHeader($Line, $Block);
if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))
{
$attributeString = $matches[1][0];
$Block['element']['attributes'] = $this->parseAttributeData($attributeString);
$Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);
}
return $Block;
}
#
# Inline Elements
#
#
# Footnote Marker
protected function inlineFootnoteMarker($Excerpt)
{
if (preg_match('/^\[\^(.+?)\]/', $Excerpt['text'], $matches))
{
$name = $matches[1];
if ( ! isset($this->DefinitionData['Footnote'][$name]))
{
return;
}
$this->DefinitionData['Footnote'][$name]['count'] ++;
if ( ! isset($this->DefinitionData['Footnote'][$name]['number']))
{
$this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount; # » &
}
$Element = array(
'name' => 'sup',
'attributes' => array('id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name),
'element' => array(
'name' => 'a',
'attributes' => array('href' => '#fn:'.$name, 'class' => 'footnote-ref'),
'text' => $this->DefinitionData['Footnote'][$name]['number'],
),
);
return array(
'extent' => strlen($matches[0]),
'element' => $Element,
);
}
}
private $footnoteCount = 0;
#
# Link
protected function inlineLink($Excerpt)
{
$Link = parent::inlineLink($Excerpt);
$remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : '';
if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches))
{
$Link['element']['attributes'] += $this->parseAttributeData($matches[1]);
$Link['extent'] += strlen($matches[0]);
}
return $Link;
}
#
# ~
#
private $currentAbreviation;
private $currentMeaning;
protected function insertAbreviation(array $Element)
{
if (isset($Element['text']))
{
$Element['elements'] = self::pregReplaceElements(
'/\b'.preg_quote($this->currentAbreviation, '/').'\b/',
array(
array(
'name' => 'abbr',
'attributes' => array(
'title' => $this->currentMeaning,
),
'text' => $this->currentAbreviation,
)
),
$Element['text']
);
unset($Element['text']);
}
return $Element;
}
protected function inlineText($text)
{
$Inline = parent::inlineText($text);
if (isset($this->DefinitionData['Abbreviation']))
{
foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)
{
$this->currentAbreviation = $abbreviation;
$this->currentMeaning = $meaning;
$Inline['element'] = $this->elementApplyRecursiveDepthFirst(
array($this, 'insertAbreviation'),
$Inline['element']
);
}
}
return $Inline;
}
#
# Util Methods
#
protected function addDdElement(array $Line, array $Block)
{
$text = substr($Line['text'], 1);
$text = trim($text);
unset($Block['dd']);
$Block['dd'] = array(
'name' => 'dd',
'handler' => array(
'function' => 'lineElements',
'argument' => $text,
'destination' => 'elements'
),
);
if (isset($Block['interrupted']))
{
$Block['dd']['handler']['function'] = 'textElements';
unset($Block['interrupted']);
}
$Block['element']['elements'] []= & $Block['dd'];
return $Block;
}
protected function buildFootnoteElement()
{
$Element = array(
'name' => 'div',
'attributes' => array('class' => 'footnotes'),
'elements' => array(
array('name' => 'hr'),
array(
'name' => 'ol',
'elements' => array(),
),
),
);
uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');
foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData)
{
if ( ! isset($DefinitionData['number']))
{
continue;
}
$text = $DefinitionData['text'];
$textElements = parent::textElements($text);
$numbers = range(1, $DefinitionData['count']);
$backLinkElements = array();
foreach ($numbers as $number)
{
$backLinkElements[] = array('text' => ' ');
$backLinkElements[] = array(
'name' => 'a',
'attributes' => array(
'href' => "#fnref$number:$definitionId",
'rev' => 'footnote',
'class' => 'footnote-backref',
),
'rawHtml' => '&#8617;',
'allowRawHtmlInSafeMode' => true,
'autobreak' => false,
);
}
unset($backLinkElements[0]);
$n = count($textElements) -1;
if ($textElements[$n]['name'] === 'p')
{
$backLinkElements = array_merge(
array(
array(
'rawHtml' => '&#160;',
'allowRawHtmlInSafeMode' => true,
),
),
$backLinkElements
);
unset($textElements[$n]['name']);
$textElements[$n] = array(
'name' => 'p',
'elements' => array_merge(
array($textElements[$n]),
$backLinkElements
),
);
}
else
{
$textElements[] = array(
'name' => 'p',
'elements' => $backLinkElements
);
}
$Element['elements'][1]['elements'] []= array(
'name' => 'li',
'attributes' => array('id' => 'fn:'.$definitionId),
'elements' => array_merge(
$textElements
),
);
}
return $Element;
}
# ~
protected function parseAttributeData($attributeString)
{
$Data = array();
$attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);
foreach ($attributes as $attribute)
{
if ($attribute[0] === '#')
{
$Data['id'] = substr($attribute, 1);
}
else # "."
{
$classes []= substr($attribute, 1);
}
}
if (isset($classes))
{
$Data['class'] = implode(' ', $classes);
}
return $Data;
}
# ~
protected function processTag($elementMarkup) # recursive
{
# http://stackoverflow.com/q/1148928/200145
libxml_use_internal_errors(true);
$DOMDocument = new DOMDocument;
# http://stackoverflow.com/q/11309194/200145
$elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8');
# http://stackoverflow.com/q/4879946/200145
$DOMDocument->loadHTML($elementMarkup);
$DOMDocument->removeChild($DOMDocument->doctype);
$DOMDocument->replaceChild($DOMDocument->firstChild->firstChild->firstChild, $DOMDocument->firstChild);
$elementText = '';
if ($DOMDocument->documentElement->getAttribute('markdown') === '1')
{
foreach ($DOMDocument->documentElement->childNodes as $Node)
{
$elementText .= $DOMDocument->saveHTML($Node);
}
$DOMDocument->documentElement->removeAttribute('markdown');
$elementText = "\n".$this->text($elementText)."\n";
}
else
{
foreach ($DOMDocument->documentElement->childNodes as $Node)
{
$nodeMarkup = $DOMDocument->saveHTML($Node);
if ($Node instanceof DOMElement and ! in_array($Node->nodeName, $this->textLevelElements))
{
$elementText .= $this->processTag($nodeMarkup);
}
else
{
$elementText .= $nodeMarkup;
}
}
}
# because we don't want for markup to get encoded
$DOMDocument->documentElement->nodeValue = 'placeholder\x1A';
$markup = $DOMDocument->saveHTML($DOMDocument->documentElement);
$markup = str_replace('placeholder\x1A', $elementText, $markup);
return $markup;
}
# ~
protected function sortFootnotes($A, $B) # callback
{
return $A['number'] - $B['number'];
}
#
# Fields
#
protected $regexAttribute = '(?:[#.][-\w]+[ ]*)';
}

View File

@ -42,6 +42,8 @@ class SiteMeta {
(PageMeta::getConfigurationLevelPage("customization.article.regex.highlight")=="false")?
null:"//cdn.jsdelivr.net/gh/suk-ws/regex-colorizer@master/regex-colorizer-default.min.css",
"/assets/bread-card-markdown.css?ver=1",
"/assets/bread-card-markdown-footnote.css",
"/assets/bread-card-markdown-task-list.css",
(PageMeta::getConfigurationLevelPage("customization.article.listing.rainbow.marker")=="true"?
"/assets/bread-card-markdown-enhanced-listing-rainbow.css?ver=1":null),
"/assets/bread-card-markdown-compat-highlight-js.css?ver=1",

View File

@ -0,0 +1,5 @@
<?php
class ExtensionRef {
}

View File

@ -0,0 +1,60 @@
<?php
use League\CommonMark\ConverterInterface;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Attributes\AttributesExtension;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DescriptionList\DescriptionListExtension;
use League\CommonMark\Extension\Footnote\FootnoteExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
class Parser {
private static ?ConverterInterface $converter = null;
public static function getDefaultParser (): ConverterInterface {
// MarkDown Parser:
// CommonMark
$parserEnv = new Environment();
$parserEnv->addExtension(new CommonMarkCoreExtension());
// from GitHub Flavor Markdown
// + autolink [https://link.to]
// + strikethrough ~~removed~~
// + table <table>
// + task list [x]
// - disallowed raw html <style><script>
$parserEnv->addExtension(new AutolinkExtension());
$parserEnv->addExtension(new StrikethroughExtension());
$parserEnv->addExtension(new TableExtension());
$parserEnv->addExtension(new TaskListExtension());
// from Kramdown (PHP Markdown Extra?)
// + html attributes {#title-1}
$parserEnv->addExtension(new AttributesExtension());
// from PHP Markdown Extra
// + footnote [^1]
// + description list <dl><dt><dd>
$parserEnv->addExtension(new FootnoteExtension());
$parserEnv->addExtension(new DescriptionListExtension());
return new MarkdownConverter($parserEnv);
}
private static function getParser (): ConverterInterface {
if (Parser::$converter === null)
Parser::$converter = self::getDefaultParser();
return Parser::$converter;
}
public static function parse (string $article): string {
return self::getParser()->convert($article);
}
}

View File

@ -1,9 +1,8 @@
<?php
require_once "./lib/Parsedown/Parsedown.php";
require_once "./lib/Parsedown/ParsedownExtra.php";
use Erusev\Parsedown\Parsedown;
class ParsedownExtend extends ParsedownExtra {
class ParsedownExtend extends Parsedown {
function __construct() {

View File

@ -1,15 +1,15 @@
<?php
require_once "./src/Utils/ParsedownExtend.php";
require_once "./src/Data/PageMeta.php";
$parser = new ParsedownExtend();
$parser->setMarkupEscaped(false);
$parser->setSafeMode(false);
require_once "./src/Utils/Markdown/Parser.php";
$pageMarkdownContent = PageMeta::$page->getMarkdownContent();
// if the `compatibility.article.title.oldversion` is enabled
// that means the title should be auto-generated from book.xml
// but not written on page.md.
// this code will generate a title from book.xml if the start
// of the page.md is not `# Title` formatting page title.
if (PageMeta::compatibilityOldTitlePolicy()) {
$length = strlen($pageMarkdownContent);
for ($i=0; $i<$length; $i++) {
@ -24,4 +24,4 @@ if (PageMeta::compatibilityOldTitlePolicy()) {
}
}
echo $parser->text($pageMarkdownContent);
echo Parser::parse($pageMarkdownContent);