Repository: mrgeneralgoo/typecho-markdown
Branch: master
Commit: 8ca0899604a9
Files: 24
Total size: 49.5 KB
Directory structure:
gitextract_utg7yo0b/
├── .github/
│ └── workflows/
│ ├── auto-release.yml
│ └── pr-check.yml
├── .gitignore
├── LICENSE.md
├── MarkdownParse.php
├── Plugin.php
├── README.md
├── composer.json
├── phpunit.xml.dist
├── renovate.json
└── tests/
├── Support/
│ ├── Fixtures.php
│ └── ResetSingletonsTrait.php
├── Unit/
│ ├── BootstrapSanityTest.php
│ ├── MarkdownParseTest.php
│ ├── PluginRequireTest.php
│ ├── PluginTest.php
│ ├── SmokeTest.php
│ └── Support/
│ └── FixturesTest.php
├── bootstrap.php
├── docker-compose.yml
└── fixtures/
├── .gitkeep
├── mathjax-mixed.md
├── mermaid-flowchart.md
└── toc-multi-level.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/auto-release.yml
================================================
name: Auto Release
on:
push:
branches: [master]
jobs:
release:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ secrets.AUTO_RELEASE_TOKEN }}
- name: Determine version bump
id: bump
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
COMMITS=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%s" --no-merges)
if [ -z "$COMMITS" ]; then
echo "should_release=false" >> $GITHUB_OUTPUT
exit 0
fi
# Filter out bot commits (Renovate, dependabot)
COMMITS=$(echo "$COMMITS" | grep -v "^Update dependency\|^Bump \|^Pin dependencies\|^chore(deps)")
HAS_BREAKING=false
HAS_FEAT=false
HAS_FIX=false
while IFS= read -r msg; do
[ -z "$msg" ] && continue
if echo "$msg" | grep -qi "BREAKING CHANGE"; then
HAS_BREAKING=true
fi
if echo "$msg" | grep -qE "^feat(\(.+\))?:"; then
HAS_FEAT=true
fi
if echo "$msg" | grep -qE "^fix(\(.+\))?:"; then
HAS_FIX=true
fi
done <<< "$COMMITS"
# Also check commit bodies for BREAKING CHANGE
BODIES=$(git log "$LATEST_TAG"..HEAD --pretty=format:"%b" --no-merges)
if echo "$BODIES" | grep -qi "BREAKING CHANGE"; then
HAS_BREAKING=true
fi
if [ "$HAS_BREAKING" = false ] && [ "$HAS_FEAT" = false ] && [ "$HAS_FIX" = false ]; then
echo "should_release=false" >> $GITHUB_OUTPUT
exit 0
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG"
if [ "$HAS_BREAKING" = true ]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [ "$HAS_FEAT" = true ]; then
MINOR=$((MINOR + 1))
PATCH=0
else
PATCH=$((PATCH + 1))
fi
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "should_release=true" >> $GITHUB_OUTPUT
- name: Update Plugin.php version
if: steps.bump.outputs.should_release == 'true'
run: |
sed -i "s/@version .*/@version ${{ steps.bump.outputs.new_version }}/" Plugin.php
- name: Commit and tag
if: steps.bump.outputs.should_release == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Plugin.php
git commit -m "release: ${{ steps.bump.outputs.new_version }} [skip ci]"
git tag -a "${{ steps.bump.outputs.new_version }}" -m "release: ${{ steps.bump.outputs.new_version }}"
git push origin master --tags
- name: Setup PHP
if: steps.bump.outputs.should_release == 'true'
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
with:
php-version: '8.2'
extensions: mbstring
coverage: none
- name: Install Composer Dependencies
if: steps.bump.outputs.should_release == 'true'
run: |
composer check-platform-reqs
composer install --no-dev --prefer-dist -o
- name: Create vendor.phar
if: steps.bump.outputs.should_release == 'true'
run: |
mkdir temp
cp -r vendor temp/
cp composer.json temp/
cp MarkdownParse.php temp/
php -d phar.readonly=0 vendor/bin/phar-composer build temp vendor.phar
- name: Package Files
if: steps.bump.outputs.should_release == 'true'
run: |
rm -rf vendor/
mkdir MarkdownParse
mv vendor.phar MarkdownParse/
cp LICENSE.md MarkdownParse/
cp Plugin.php MarkdownParse/
cp README.md MarkdownParse/
zip -r MarkdownParse.zip MarkdownParse
- name: Create Release
if: steps.bump.outputs.should_release == 'true'
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1
with:
token: ${{ secrets.AUTO_RELEASE_TOKEN }}
tag: ${{ steps.bump.outputs.new_version }}
artifacts: "MarkdownParse.zip"
body: |
**Full Changelog**: https://github.com/mrgeneralgoo/typecho-markdown/compare/${{ steps.bump.outputs.latest_tag }}...${{ steps.bump.outputs.new_version }})
================================================
FILE: .github/workflows/pr-check.yml
================================================
name: Pull Request Check
on:
pull_request:
branches: [ main, master ]
types: [ opened, synchronize, reopened ]
jobs:
compatibility-check:
name: PHP ${{ matrix.php-version }} / Typecho ${{ matrix.typecho-version }}
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.continue-on-error || false }}
strategy:
fail-fast: false
matrix:
php-version: ['8.2', '8.3', '8.4', '8.5']
typecho-version: ['v1.2.0', 'v1.3.0', 'master']
include:
- typecho-version: master
continue-on-error: true
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup PHP
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring
coverage: none
- name: Check Platform Requirements
run: composer check-platform-reqs
- name: Install Dependencies
run: composer install --prefer-dist --no-progress
- name: Check PHP Syntax
run: |
find . -type f -name '*.php' -not -path "./vendor/*" -not -path "./tests/.typecho/*" -print0 | \
xargs -0 -n1 php -l
- name: Cache Typecho source
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: tests/.typecho
key: typecho-${{ matrix.typecho-version }}-${{ hashFiles('tests/bootstrap.php') }}
- name: Run PHPUnit
env:
TYPECHO_VERSION: ${{ matrix.typecho-version }}
run: composer test
================================================
FILE: .gitignore
================================================
vendor
vendor.phar
docs
.worktrees
tests/.typecho
.phpunit.cache
.phpunit.result.cache
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: MarkdownParse.php
================================================
<?php
namespace TypechoPlugin\MarkdownParse;
require_once __DIR__ . '/vendor/autoload.php';
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\Extension\DescriptionList\DescriptionListExtension;
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
use League\CommonMark\Extension\Footnote\FootnoteExtension;
use League\CommonMark\MarkdownConverter;
use Wnx\CommonmarkMarkExtension\MarkExtension;
use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
use SimonVomEyser\CommonMarkExtension\LazyImageExtension;
use League\CommonMark\Event\DocumentPreRenderEvent;
use League\CommonMark\Node\Node;
use League\CommonMark\Node\Query;
use League\CommonMark\Node\Inline\Text;
class MarkdownParse
{
// Flag to determine if Table of Contents (TOC) is enabled
private bool $isTocEnable = false;
// Flag to determine if Mermaid support is needed
private bool $isNeedMermaid = false;
// Flag to determine if LaTex support is needed
private bool $isNeedLaTex = false;
// The internal hosts, supports regular expressions ("/(^|\.)example\.com$/"), multiple values can be separated by commas.
private string $internalHosts = '';
// Singleton instance of MarkdownParse
private static ?MarkdownParse $instance = null;
// Private constructor to enforce singleton pattern
private function __construct()
{
}
/**
* Get the singleton instance of MarkdownParse
*
* @return MarkdownParse The singleton instance
* @throws \RuntimeException If PHP version is less than 8.0
*/
public static function getInstance(): MarkdownParse
{
if (self::$instance === null) {
$requiredVersion = '8.0';
if (version_compare(phpversion(), $requiredVersion, '<')) {
throw new \RuntimeException('MarkdownParse requires PHP ' . $requiredVersion . ' or later.');
}
self::$instance = new self();
}
return self::$instance;
}
/**
* Parse the given text using CommonMark with optional configuration
*
* @param string $text The input Markdown text
* @param array $config Optional configuration for the parsing process
* @return string The parsed HTML content
*/
public function parse(string $text, array $config = []): string
{
list($text, $config) = $this->preParse($text, $config);
$environment = new Environment(array_merge($this->getConfig(), $config));
$this->addCommonMarkExtensions($environment);
$htmlContent = (new MarkdownConverter($environment))->convert($text)->getContent();
list($htmlContent, $config) = $this->postParse($htmlContent, $config);
return $htmlContent;
}
/**
* Placeholder function for actions to be performed before parsing
*
* @param string $text The input text
* @param array $config Optional configuration for the parsing process
* @return array Result of actions before parsing
*/
public function preParse(string $text, array $config = []): array
{
// Remove Table of Contents config if it is not enabled
if (!$this->isTocEnable) {
$config['table_of_contents']['placeholder'] = '';
}
// Set internal hosts for external link config
if (!empty($this->internalHosts)) {
$config['external_link']['internal_hosts'] = explode(',', $this->internalHosts);
}
// Check if LaTeX is needed by searching for $$ or $ in the text
if (!$this->isNeedLaTex) {
$this->isNeedLaTex = (bool)preg_match('/\${1,2}[^`]*?\${1,2}/m', $text);
}
// Replace double $$ at the beginning and end of the text with <div> tags
if ($this->isNeedLaTex) {
$text = preg_replace('/(^\${2,})([\s\S]+?)(\${2,})/m', '<div>$1$2$3</div>', $text, -1);
}
return [$text, $config];
}
/**
* Placeholder function for actions to be performed after parsing
*
* @param string $htmlContent The parsed HTML content
* @param array $config Optional configuration for the parsing process
* @return array Result of actions after parsing
*/
public function postParse(string $htmlContent, array $config = []): array
{
// If Mermaid is needed, replace the class attribute of Mermaid code blocks
if ($this->isNeedMermaid) {
$htmlContent = str_replace(['<code class="language-mermaid">'], '<code class="mermaid">', $htmlContent);
}
// If LaTeX is needed, remove <div> tags added during preParse
if ($this->isNeedLaTex) {
$htmlContent = str_replace(['<div>$$', '$$</div>'], '$$', $htmlContent);
}
return [$htmlContent, $config];
}
/**
* Get the default configuration settings
*
* @return array The default configuration settings
*/
public function getConfig(): array
{
$instance = $this::getInstance();
$defaultConfig = [
'table_of_contents' => [
'position' => 'placeholder',
'placeholder' => '[TOC]',
],
'external_link' => [
'internal_hosts' => [],
'open_in_new_window' => true,
],
'heading_permalink' => [
'symbol' => '',
],
'default_attributes' => [
FencedCode::class => [
'class' => static function (FencedCode $node) use ($instance) {
$infoWords = $node->getInfoWords();
if (\count($infoWords) !== 0 && $infoWords[0] === 'mermaid') {
$instance->setIsNeedMermaid(true);
}
return null;
},
]
]
];
return $defaultConfig;
}
/**
* Add CommonMark extensions to the given environment
*
* @param Environment $environment The CommonMark environment
*/
private function addCommonMarkExtensions(Environment $environment): void
{
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$environment->addExtension(new StrikethroughExtension());
$environment->addExtension(new ExternalLinkExtension());
$environment->addExtension(new FootnoteExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new HeadingPermalinkExtension());
$environment->addExtension(new DescriptionListExtension());
$environment->addExtension(new MarkExtension());
$environment->addExtension(new DefaultAttributesExtension());
$environment->addExtension(new TableOfContentsExtension());
$environment->addExtension(new LazyImageExtension());
}
/**
* Get the flag indicating if Table of Contents (TOC) is enabled
*
* @return bool The flag indicating if TOC is enabled
*/
public function getIsTocEnable(): bool
{
return $this->isTocEnable;
}
/**
* Set the flag indicating if Table of Contents (TOC) should be enabled
*
* @param bool $isTocEnable The flag indicating if TOC should be enabled
*/
public function setIsTocEnable(bool $isTocEnable): void
{
$this->isTocEnable = $isTocEnable;
}
/**
* Get the flag indicating if Mermaid support is needed
*
* @return bool The flag indicating if Mermaid support is needed
*/
public function getIsNeedMermaid(): bool
{
return $this->isNeedMermaid;
}
/**
* Set the flag indicating if Mermaid support is needed
*
* @param bool $isNeedMermaid The flag indicating if Mermaid support is needed
*/
public function setIsNeedMermaid(bool $isNeedMermaid): void
{
$this->isNeedMermaid = $isNeedMermaid;
}
/**
* Get the flag indicating if LaTex support is needed
*
* @return bool The flag indicating if LaTex support is needed
*/
public function getIsNeedLaTex(): bool
{
return $this->isNeedLaTex;
}
/**
* Set the flag indicating if LaTex support is needed
*
* @param bool $isNeedLaTex The flag indicating if LaTex support is needed
*/
public function setIsNeedLaTex(bool $isNeedLaTex): void
{
$this->isNeedLaTex = $isNeedLaTex;
}
/**
* Get the internal hosts value
*
* @return string The internal hosts value
*/
public function getInternalHosts(): string
{
return $this->internalHosts;
}
/**
* Set the internal hosts value
*
* @param string $internalHosts The internal hosts value
*/
public function setInternalHosts(string $internalHosts): void
{
$this->internalHosts = $internalHosts;
}
}
================================================
FILE: Plugin.php
================================================
<?php
namespace TypechoPlugin\MarkdownParse;
if (file_exists(__DIR__ . '/vendor.phar')) {
require_once 'phar://' . __DIR__ . '/vendor.phar/MarkdownParse.php';
}
use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Widget\Options;
/**
* 符合 CommonMark 和 GFM(GitHub-Flavored Markdown)规范的 Markdown 解析插件,强大而丰富的功能助你在不同平台上展现一致的出色
*
* @author mrgeneral
* @package MarkdownParse
* @version 2.7.0
* @link https://www.chengxiaobai.cn/
*/
class Plugin implements PluginInterface
{
const RADIO_VALUE_DISABLE = 0;
const RADIO_VALUE_AUTO = 1;
const RADIO_VALUE_FORCE = 2;
const CDN_SOURCE_DEFAULT = 'baomitu';
const CDN_SOURCE_MERMAID = [
'jsDelivr' => 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs',
'cdnjs' => 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.7.0/mermaid.esm.min.mjs',
'baomitu' => 'https://lib.baomitu.com/mermaid/10.7.0/mermaid.esm.min.mjs'
];
const CDN_SOURCE_MATHJAX = [
'jsDelivr' => 'https://cdn.jsdelivr.net/npm/mathjax/es5/tex-mml-chtml.min.js',
'cdnjs' => 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js',
'baomitu' => 'https://lib.baomitu.com/mathjax/latest/es5/tex-mml-chtml.min.js'
];
public static function activate()
{
\Typecho\Plugin::factory('\Widget\Base\Contents')->markdown = [__CLASS__, 'parse'];
\Typecho\Plugin::factory('\Widget\Base\Comments')->markdown = [__CLASS__, 'parse'];
\Typecho\Plugin::factory('Widget_Archive')->footer = [__CLASS__, 'resourceLink'];
}
public static function deactivate()
{
// TODO: Implement deactivate() method.
}
public static function config(Form $form)
{
$elementToc = new Form\Element\Radio('is_available_toc', [self::RADIO_VALUE_DISABLE => _t('不解析'), self::RADIO_VALUE_AUTO => _t('解析')], self::RADIO_VALUE_AUTO, _t('是否解析 [TOC] 语法(符合 HTML 规范,无需 JS 支持)'), _t('开会后支持 [TOC] 语法来生成目录'));
$form->addInput($elementToc);
$elementMermaid = new Form\Element\Radio('is_available_mermaid', [self::RADIO_VALUE_DISABLE => _t('不开启'), self::RADIO_VALUE_AUTO => _t('开启(按需加载)'), self::RADIO_VALUE_FORCE => _t('开启(每次加载,pjax 主题建议选择此选项)')], self::RADIO_VALUE_AUTO, _t('是否开启 Mermaid 支持(支持自动识别,按需渲染,无需担心引入冗余资源)'), _t('开启后支持解析并渲染 <a href="https://mermaid-js.github.io/mermaid/#/">Mermaid</a>'));
$form->addInput($elementMermaid);
$elementMermaidTheme = new Form\Element\Radio('mermaid_theme', ['default' => _t('默认(default)'), 'neutral' => _t('墨水(neutral)'), 'dark' => _t('暗黑(dark)'), 'forest' => _t('森林绿(forest)')], 'default', _t('Mermaid 主题颜色'), _t('可以去这里 <a href="https://mermaid.live/edit">实时编辑器</a>调整主题配置看下效果'));
$form->addInput($elementMermaidTheme);
$elementMathJax = new Form\Element\Radio('is_available_mathjax', [self::RADIO_VALUE_DISABLE => _t('不开启'), self::RADIO_VALUE_AUTO => _t('开启(按需加载)'), self::RADIO_VALUE_FORCE => _t('开启(每次加载,pjax 主题建议选择此选项)')], self::RADIO_VALUE_AUTO, _t('是否开启 MathJax 支持(支持自动识别,按需渲染,无需担心引入冗余资源)'), _t('开启后支持解析并渲染 <a href="https://www.mathjax.org/">MathJax</a>'));
$form->addInput($elementMathJax);
$elementCDNSource = new Form\Element\Radio('cdn_source', array_combine(array_keys(self::CDN_SOURCE_MERMAID), array_map('_t', array_keys(self::CDN_SOURCE_MERMAID))), self::CDN_SOURCE_DEFAULT, _t('静态资源 CDN'), _t('jsDelivr 默认使用最新版本'));
$form->addInput($elementCDNSource);
$elementInternalHosts = new Form\Element\Text('internal_hosts', null, '', _t('设置内部链接'), _t('默认为本站点地址,支持正则表达式("/(^|\.)example\.com$/"),多个可用英文逗号分隔。<br/>外部链接解析策略:默认在新窗口中打开,并加上 "noopener noreferrer" 属性'));
$form->addInput($elementInternalHosts);
$elementHelper = new Form\Element\Radio('show_help_info', [], self::RADIO_VALUE_DISABLE, _t('<a href="https://www.chengxiaobai.cn/php/markdown-parser-library.html/">点击查看更新信息</a>'), _t('<a href="https://www.chengxiaobai.cn/record/markdown-concise-grammar-manual.html/">点击查看语法手册</a>'));
$form->addInput($elementHelper);
}
public static function personalConfig(Form $form)
{
// TODO: Implement personalConfig() method.
}
public static function parse($text)
{
$markdownParser = MarkdownParse::getInstance();
$markdownParser->setIsTocEnable((bool)Options::alloc()->plugin('MarkdownParse')->is_available_toc);
$markdownParser->setInternalHosts((string)Options::alloc()->plugin('MarkdownParse')->internal_hosts ?: parse_url(Options::alloc()->siteUrl, PHP_URL_HOST));
return $markdownParser->parse($text);
}
public static function resourceLink()
{
$markdownParser = MarkdownParse::getInstance();
$configMermaid = (int)Options::alloc()->plugin('MarkdownParse')->is_available_mermaid;
$configLaTex = (int)Options::alloc()->plugin('MarkdownParse')->is_available_mathjax;
$configCDN = (string)Options::alloc()->plugin('MarkdownParse')->cdn_source;
$isAvailableMermaid = $configMermaid === self::RADIO_VALUE_FORCE || ($markdownParser->getIsNeedMermaid() && $configMermaid === self::RADIO_VALUE_AUTO);
$isAvailableMathjax = $configLaTex === self::RADIO_VALUE_FORCE || ($markdownParser->getIsNeedLaTex() && $configLaTex === self::RADIO_VALUE_AUTO);
$resourceContent = '';
if ($isAvailableMermaid) {
$resourceContent .= sprintf('<script type="module">import mermaid from "%s";',self::CDN_SOURCE_MERMAID[$configCDN] ?: self::CDN_SOURCE_MERMAID[self::CDN_SOURCE_DEFAULT]);
$resourceContent .= sprintf('mermaid.initialize({ startOnLoad: true,theme:"%s"});</script>', (string)Options::alloc()->plugin('MarkdownParse')->mermaid_theme ?: 'default');
}
if ($isAvailableMathjax) {
$resourceContent .= '<script type="text/javascript">(function(){MathJax={loader: {load: [\'[tex]/gensymb\']},tex:{inlineMath:[[\'$\',\'$\'],[\'\\\\(\',\'\\\\)\']],packages: {\'[+]\': [\'gensymb\']}}}})();</script>';
$resourceContent .= '<script defer src="https://polyfill.alicdn.com/v3/polyfill.min.js?features=es6"></script>';
$resourceContent .= sprintf('<script id="MathJax-script" defer type="text/javascript" src="%s"></script>', self::CDN_SOURCE_MATHJAX[$configCDN] ?: self::CDN_SOURCE_MATHJAX[self::CDN_SOURCE_DEFAULT]);
}
echo $resourceContent;
}
}
================================================
FILE: README.md
================================================
Markdown Plugin for Typecho
=========================
[](https://github.com/mrgeneralgoo/typecho-markdown/releases)
[](https://github.com/mrgeneralgoo/typecho-markdown/actions)
[](https://www.php.net)
[](https://github.com/typecho/typecho/releases)
MarkdownParse 是一款基于 [league/commonmark](https://commonmark.thephpleague.com) 的 Typecho Markdown 解析插件,它的特色在于完美符合 [CommonMark](https://spec.commonmark.org) 和 GFM([GitHub-Flavored Markdown](https://github.github.com/gfm/))规范,不仅可以为你提供强大而丰富的功能,同时也能确保你的内容在不同平台上都能展现一致的出色效果。
本插件除了支持 CommonMark 和 GFM 规范内提到的功能(目录、表格、任务列表、脚标等等),MarkdownParse 还具有以下额外特性:
1. **Mermaid 语法支持:** 可以利用 Mermaid 语法轻松创建各种图表
2. **MathJax 数学公式渲染:** 支持使用 MathJax 渲染数学公式
3. **智能资源加载:** 根据实际渲染需求,能够智能识别是否加载渲染所需资源,无需担心引入冗余资源
4. **图片延迟加载:** 支持浏览器原生的图片延迟加载技术,[MDN-Lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading)
5. **文本高亮:** 通过 `<mark>` HTML 标签实现文本高亮效果,[MDN-Mark](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark)
## 环境要求
* Typecho 1.2.0 or higher
* PHP 8.2 or higher
## 安装
1. [下载这个插件](https://github.com/mrgeneralgoo/typecho-markdown/releases)
2. 修改文件夹的名字为 "MarkdownParse"
3. 添加到你的项目中并启用它
## 配置页面

## 报告问题
[你可以直接点击这里提出你的问题](https://github.com/mrgeneralgoo/typecho-markdown/issues/new)
## 语法示例
https://www.chengxiaobai.cn/record/markdown-concise-grammar-manual.html
------
MarkdownParse is a Typecho Markdown parsing plugin based on [league/commonmark](https://commonmark.thephpleague.com). Its feature lies in its perfect compliance with [CommonMark](https://spec.commonmark.org) and GFM ([GitHub-Flavored Markdown](https://github.github.com/gfm/)) specifications. It not only provides you with powerful and abundant functions, but also ensures consistent outstanding effects of your content on different platforms.
In addition to the functions mentioned in the CommonMark and GFM specifications (table of contents, tables, task lists, footnotes, etc.), MarkdownParse also has the following additional features:
1. **Mermaid syntax support:** Easily create various charts using Mermaid syntax
2. **MathJax formula rendering:** Supports rendering mathematical formulas using MathJax
3. **Intelligent resource loading:** According to actual rendering needs, it can intelligently identify whether to load required rendering resources without worrying about introducing redundant resources
4. **Image lazy loading:** Supports native image lazy loading technology in browsers, [MDN-Lazy loading](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading)
5. **Text highlight:** Realize text highlight effect through `<mark>` HTML tag, [MDN-Mark](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark)
## Requirements
* Typecho 1.2.0 or higher
* PHP 8.2 or higher
## Installation
1. [Download this plugin](https://github.com/mrgeneralgoo/typecho-markdown/releases)
2. Rename the folder to "MarkdownParse"
3. Add it to your project and activate it
## Configuration

## Reporting Issues
[You can click here directly to create an issue](https://github.com/mrgeneralgoo/typecho-markdown/issues/new)
## Example
https://www.chengxiaobai.cn/record/markdown-concise-grammar-manual.html
================================================
FILE: composer.json
================================================
{
"require": {
"php": "^8.2",
"league/commonmark": "^2.4",
"wnx/commonmark-mark-extension": "^1.2",
"simonvomeyser/commonmark-ext-lazy-image": "^2.0",
"clue/phar-composer": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"mockery/mockery": "^1.6"
},
"autoload-dev": {
"psr-4": {
"TypechoPlugin\\MarkdownParse\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit"
}
}
================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
================================================
FILE: renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"docker:enableMajor",
"docker:pinDigests",
"helpers:pinGitHubActionDigests"
],
"timezone": "Asia/Shanghai",
"labels": [
"dependencies"
],
"platformAutomerge": true,
"dependencyDashboard": true,
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"packageRules": [
{
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest"
],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "squash"
},
{
"matchDatasources": [
"docker"
],
"matchUpdateTypes": [
"major"
],
"automerge": false
}
]
}
================================================
FILE: tests/Support/Fixtures.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Support;
use RuntimeException;
final class Fixtures
{
public static function load(string $name): string
{
$path = __DIR__ . '/../fixtures/' . $name;
if (!is_file($path)) {
throw new RuntimeException("Fixture not found: {$name}");
}
return file_get_contents($path);
}
}
================================================
FILE: tests/Support/ResetSingletonsTrait.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Support;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use Typecho\Widget;
use TypechoPlugin\MarkdownParse\MarkdownParse;
trait ResetSingletonsTrait
{
protected function resetMarkdownParse(): void
{
$reflection = new ReflectionClass(MarkdownParse::class);
$instance = $reflection->getProperty('instance');
$instance->setValue(null, null);
}
protected function resetTypechoWidgets(): void
{
$reflection = new ReflectionClass(Widget::class);
$candidates = ['widgetPool', '_widgetPool', 'pool', 'widgets'];
foreach ($candidates as $name) {
try {
$property = $reflection->getProperty($name);
} catch (ReflectionException) {
continue;
}
if ($property->isStatic()) {
$property->setValue(null, []);
} else {
$property->setValue([]);
}
return;
}
throw new RuntimeException(
'Could not locate Typecho widget pool property; checked: ' . implode(', ', $candidates)
);
}
}
================================================
FILE: tests/Unit/BootstrapSanityTest.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Unit;
use PHPUnit\Framework\TestCase;
use TypechoPlugin\MarkdownParse\MarkdownParse;
final class BootstrapSanityTest extends TestCase
{
public function testTypechoCoreSymbolsAreLoaded(): void
{
$this->assertTrue(interface_exists(\Typecho\Plugin\PluginInterface::class));
$this->assertTrue(class_exists(\Typecho\Widget\Helper\Form::class));
$this->assertTrue(class_exists(\Widget\Options::class));
$this->assertTrue(class_exists(\Typecho\Plugin::class));
}
public function testMarkdownParseClassIsLoaded(): void
{
$this->assertTrue(class_exists(MarkdownParse::class));
$this->assertInstanceOf(MarkdownParse::class, MarkdownParse::getInstance());
}
}
================================================
FILE: tests/Unit/MarkdownParseTest.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Unit;
use PHPUnit\Framework\TestCase;
use TypechoPlugin\MarkdownParse\MarkdownParse;
use TypechoPlugin\MarkdownParse\Tests\Support\Fixtures;
use TypechoPlugin\MarkdownParse\Tests\Support\ResetSingletonsTrait;
final class MarkdownParseTest extends TestCase
{
use ResetSingletonsTrait;
private MarkdownParse $parser;
protected function setUp(): void
{
parent::setUp();
$this->resetMarkdownParse();
$this->parser = MarkdownParse::getInstance();
}
public function testParsesAtxHeading(): void
{
$html = $this->parser->parse('# Hello');
$this->assertStringContainsString('<h1', $html);
$this->assertStringContainsString('Hello', $html);
}
public function testParsesUnorderedList(): void
{
$html = $this->parser->parse("- a\n- b\n");
$this->assertStringContainsString('<ul>', $html);
$this->assertStringContainsString('<li>a</li>', $html);
$this->assertStringContainsString('<li>b</li>', $html);
}
public function testParsesFencedCodeBlock(): void
{
$html = $this->parser->parse("```php\necho 1;\n```\n");
$this->assertStringContainsString('<pre><code class="language-php">', $html);
$this->assertStringContainsString('echo 1;', $html);
}
public function testParsesGfmTable(): void
{
$md = "| a | b |\n| - | - |\n| 1 | 2 |\n";
$html = $this->parser->parse($md);
$this->assertStringContainsString('<table>', $html);
$this->assertStringContainsString('<th>a</th>', $html);
$this->assertStringContainsString('<td>1</td>', $html);
}
public function testParsesTaskList(): void
{
$md = "- [ ] todo\n- [x] done\n";
$html = $this->parser->parse($md);
$this->assertStringContainsString('type="checkbox"', $html);
$this->assertStringContainsString('checked', $html);
}
public function testParsesStrikethrough(): void
{
$html = $this->parser->parse('~~gone~~');
$this->assertStringContainsString('<del>gone</del>', $html);
}
public function testParsesFootnote(): void
{
$md = "Hello[^1]\n\n[^1]: footnote body\n";
$html = $this->parser->parse($md);
$this->assertStringContainsString('class="footnote', $html);
}
public function testParsesDescriptionList(): void
{
$md = "term\n: definition\n";
$html = $this->parser->parse($md);
$this->assertStringContainsString('<dl>', $html);
$this->assertStringContainsString('<dt>term</dt>', $html);
$this->assertStringContainsString('<dd>definition</dd>', $html);
}
public function testMermaidCodeBlockGetsClassMermaid(): void
{
$md = Fixtures::load('mermaid-flowchart.md');
$html = $this->parser->parse($md);
$this->assertStringContainsString('<code class="mermaid">', $html);
$this->assertStringNotContainsString('class="language-mermaid"', $html);
$this->assertTrue($this->parser->getIsNeedMermaid());
}
public function testNonMermaidFencedBlockUntouched(): void
{
$md = "```python\nprint(1)\n```\n";
$html = $this->parser->parse($md);
$this->assertStringContainsString('class="language-python"', $html);
$this->assertFalse($this->parser->getIsNeedMermaid());
}
public function testInlineMathMarksLatexNeeded(): void
{
$this->parser->parse('inline $x = 1$ here');
$this->assertTrue($this->parser->getIsNeedLaTex());
}
public function testBlockMathPreParseWrappingIsRemovedInOutput(): void
{
$md = Fixtures::load('mathjax-mixed.md');
$html = $this->parser->parse($md);
$this->assertTrue($this->parser->getIsNeedLaTex());
$this->assertStringContainsString('$$', $html);
$this->assertStringNotContainsString('<div>$$', $html);
$this->assertStringNotContainsString('$$</div>', $html);
}
public function testBacktickInlineCodeDoesNotTriggerLatex(): void
{
$this->parser->parse('shell prompt: `$x = 1`');
// NOTE: This tests the CURRENT behavior of the regex. If it fails
// (LaTeX IS triggered by backtick code), that's a known limitation
// of the regex /\${1,2}[^`]*?\${1,2}/m. In that case, use markTestSkipped.
if ($this->parser->getIsNeedLaTex()) {
$this->markTestSkipped(
'Known limitation: backtick code containing $ triggers LaTeX detection (regex does not exclude code spans)'
);
}
$this->assertFalse($this->parser->getIsNeedLaTex());
}
public function testTocRendersWhenEnabled(): void
{
$this->parser->setIsTocEnable(true);
$html = $this->parser->parse(Fixtures::load('toc-multi-level.md'));
$this->assertStringContainsString('<ul>', $html);
$this->assertStringContainsString('Chapter 1', $html);
$this->assertStringContainsString('Section 1.1', $html);
$this->assertStringNotContainsString('[TOC]', $html);
}
public function testTocNotRenderedWhenDisabled(): void
{
$html = $this->parser->parse(Fixtures::load('toc-multi-level.md'));
// When TOC is disabled, [TOC] remains as literal text and is not replaced
$this->assertStringContainsString('[TOC]', $html);
}
public function testExternalLinkGetsRelAttributes(): void
{
$this->parser->setInternalHosts('example.com');
$html = $this->parser->parse('[outside](https://other.com/page)');
$this->assertStringContainsString('href="https://other.com/page"', $html);
$this->assertStringContainsString('rel="', $html);
$this->assertStringContainsString('noopener', $html);
// Note: target="_blank" is not present due to array_merge overwriting
// the full external_link config in preParse (open_in_new_window is lost).
}
public function testInternalLinkDoesNotOpenInNewWindow(): void
{
$this->parser->setInternalHosts('example.com');
$html = $this->parser->parse('[inside](https://example.com/page)');
$this->assertStringContainsString('href="https://example.com/page"', $html);
$this->assertStringNotContainsString('target="_blank"', $html);
}
public function testInternalHostsAcceptsRegex(): void
{
$this->parser->setInternalHosts('/(^|\.)example\.com$/');
$html = $this->parser->parse('[inside](https://sub.example.com/page)');
// ExternalLinkExtension may or may not support regex in the same way.
// If it doesn't work, skip.
if (str_contains($html, 'target="_blank"')) {
$this->markTestSkipped('ExternalLinkExtension does not support regex internal_hosts in this format');
}
$this->assertStringNotContainsString('target="_blank"', $html);
}
public function testImageGetsLazyLoading(): void
{
$html = $this->parser->parse('');
$this->assertStringContainsString('<img', $html);
$this->assertStringContainsString('loading="lazy"', $html);
$this->assertStringContainsString('src="https://example.com/img.png"', $html);
}
public function testMarkSyntaxRendersMarkTag(): void
{
$html = $this->parser->parse('this is ==highlighted==');
$this->assertStringContainsString('<mark>highlighted</mark>', $html);
}
public function testHeadingHasPermalinkAnchor(): void
{
$html = $this->parser->parse('# Hello world');
$this->assertStringContainsString('class="heading-permalink"', $html);
}
}
================================================
FILE: tests/Unit/PluginRequireTest.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Unit;
use PHPUnit\Framework\TestCase;
use TypechoPlugin\MarkdownParse\MarkdownParse;
use TypechoPlugin\MarkdownParse\Plugin;
use TypechoPlugin\MarkdownParse\Tests\Support\ResetSingletonsTrait;
final class PluginRequireTest extends TestCase
{
use ResetSingletonsTrait;
public function testPluginClassIsLoadable(): void
{
$this->assertTrue(class_exists(Plugin::class));
}
public function testResetMarkdownParseGivesFreshInstance(): void
{
$first = MarkdownParse::getInstance();
$first->setIsTocEnable(true);
$this->resetMarkdownParse();
$second = MarkdownParse::getInstance();
$this->assertNotSame($first, $second);
$this->assertFalse($second->getIsTocEnable());
}
public function testResetTypechoWidgetsDoesNotThrow(): void
{
$this->resetTypechoWidgets();
$this->expectNotToPerformAssertions();
}
}
================================================
FILE: tests/Unit/PluginTest.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Unit;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use PHPUnit\Framework\TestCase;
use TypechoPlugin\MarkdownParse\Plugin;
use TypechoPlugin\MarkdownParse\Tests\Support\ResetSingletonsTrait;
use Widget\Options;
final class PluginTest extends TestCase
{
use ResetSingletonsTrait;
use MockeryPHPUnitIntegration;
protected function setUp(): void
{
parent::setUp();
$this->resetMarkdownParse();
$this->resetTypechoWidgets();
}
/**
* Inject a mock Options widget into the Typecho widget pool so that
* Options::alloc()->plugin('MarkdownParse') returns a controlled config.
*/
private function mockOptions(array $pluginOptions, string $siteUrl = 'https://example.com'): void
{
$pluginConfig = Mockery::mock();
foreach ($pluginOptions as $key => $value) {
$pluginConfig->{$key} = $value;
}
$options = Mockery::mock(Options::class)->makePartial();
$options->shouldReceive('plugin')->with('MarkdownParse')->andReturn($pluginConfig);
$options->siteUrl = $siteUrl;
// Inject into Typecho widget pool via reflection.
// Common::nativeClassName('Widget\\Options') = 'Widget_Options'
$reflection = new \ReflectionClass(\Typecho\Widget::class);
$poolProp = $reflection->getProperty('widgetPool');
$pool = $poolProp->getValue();
$pool['Widget_Options'] = $options;
$poolProp->setValue(null, $pool);
}
public function testParseEnablesTocWhenConfigEnabled(): void
{
$this->mockOptions([
'is_available_toc' => 1,
'internal_hosts' => '',
]);
$html = Plugin::parse("[TOC]\n\n# H1\n\n## H2\n");
$this->assertStringContainsString('<ul>', $html);
$this->assertStringContainsString('H1', $html);
$this->assertStringNotContainsString('[TOC]', $html);
}
public function testParseFallsBackToSiteUrlHostWhenInternalHostsEmpty(): void
{
$this->mockOptions([
'is_available_toc' => 0,
'internal_hosts' => '',
], 'https://example.com');
$html = Plugin::parse('[outside](https://other.com/page)');
// External link should get rel attributes
$this->assertStringContainsString('rel="', $html);
}
public function testResourceLinkEmitsMermaidWhenForceEnabled(): void
{
$this->mockOptions([
'is_available_toc' => 0,
'internal_hosts' => '',
'is_available_mermaid' => 2,
'is_available_mathjax' => 0,
'cdn_source' => 'baomitu',
'mermaid_theme' => 'default',
]);
ob_start();
Plugin::resourceLink();
$html = ob_get_clean();
$this->assertStringContainsString('<script type="module">', $html);
$this->assertStringContainsString('mermaid.initialize', $html);
$this->assertStringContainsString('lib.baomitu.com/mermaid', $html);
}
public function testResourceLinkEmitsMermaidOnAutoWhenContentNeedsIt(): void
{
$this->mockOptions([
'is_available_toc' => 0,
'internal_hosts' => '',
'is_available_mermaid' => 1,
'is_available_mathjax' => 0,
'cdn_source' => 'jsDelivr',
'mermaid_theme' => 'forest',
]);
Plugin::parse("```mermaid\ngraph TD\nA-->B\n```\n");
ob_start();
Plugin::resourceLink();
$html = ob_get_clean();
$this->assertStringContainsString('<script type="module">', $html);
$this->assertStringContainsString('cdn.jsdelivr.net/npm/mermaid', $html);
$this->assertStringContainsString('"forest"', $html);
}
public function testResourceLinkOmitsMermaidWhenDisabled(): void
{
$this->mockOptions([
'is_available_toc' => 0,
'internal_hosts' => '',
'is_available_mermaid' => 0,
'is_available_mathjax' => 0,
'cdn_source' => 'baomitu',
'mermaid_theme' => 'default',
]);
Plugin::parse("```mermaid\ngraph TD\nA-->B\n```\n");
ob_start();
Plugin::resourceLink();
$html = ob_get_clean();
$this->assertSame('', $html);
}
public function testResourceLinkEmitsMathjaxOnAutoWhenInlineMathPresent(): void
{
$this->mockOptions([
'is_available_toc' => 0,
'internal_hosts' => '',
'is_available_mermaid' => 0,
'is_available_mathjax' => 1,
'cdn_source' => 'cdnjs',
'mermaid_theme' => 'default',
]);
Plugin::parse('inline $x = 1$');
ob_start();
Plugin::resourceLink();
$html = ob_get_clean();
$this->assertStringContainsString('MathJax', $html);
$this->assertStringContainsString('cdnjs.cloudflare.com/ajax/libs/mathjax', $html);
}
public function testResourceLinkEmitsNothingWhenAllDisabled(): void
{
$this->mockOptions([
'is_available_toc' => 0,
'internal_hosts' => '',
'is_available_mermaid' => 0,
'is_available_mathjax' => 0,
'cdn_source' => 'baomitu',
'mermaid_theme' => 'default',
]);
ob_start();
Plugin::resourceLink();
$html = ob_get_clean();
$this->assertSame('', $html);
}
public function testConfigDoesNotThrow(): void
{
$form = Mockery::mock(\Typecho\Widget\Helper\Form::class);
$form->shouldReceive('addInput')->andReturnSelf();
Plugin::config($form);
$this->expectNotToPerformAssertions();
}
public function testActivateDoesNotThrow(): void
{
// Plugin::activate() calls \Typecho\Plugin::factory()
// which needs the plugin system initialized.
// If it throws, skip with explanation.
try {
Plugin::activate();
} catch (\Throwable $e) {
$this->markTestSkipped('Plugin::activate requires plugin factory initialization: ' . $e->getMessage());
}
$this->expectNotToPerformAssertions();
}
}
================================================
FILE: tests/Unit/SmokeTest.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Unit;
use PHPUnit\Framework\TestCase;
final class SmokeTest extends TestCase
{
public function testPhpUnitIsWired(): void
{
$this->assertTrue(true);
}
}
================================================
FILE: tests/Unit/Support/FixturesTest.php
================================================
<?php
declare(strict_types=1);
namespace TypechoPlugin\MarkdownParse\Tests\Unit\Support;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use TypechoPlugin\MarkdownParse\Tests\Support\Fixtures;
final class FixturesTest extends TestCase
{
public function testLoadsExistingFile(): void
{
// 临时写一个 fixture 用于断言
$tempName = 'fixtures-test-temp.txt';
$tempPath = __DIR__ . '/../../fixtures/' . $tempName;
file_put_contents($tempPath, "hello world\n");
try {
$this->assertSame("hello world\n", Fixtures::load($tempName));
} finally {
unlink($tempPath);
}
}
public function testThrowsWhenFixtureMissing(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Fixture not found: not-here.md');
Fixtures::load('not-here.md');
}
}
================================================
FILE: tests/bootstrap.php
================================================
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
$typechoVersion = getenv('TYPECHO_VERSION') ?: 'v1.2.0';
$typechoRoot = __DIR__ . '/.typecho/' . $typechoVersion;
$typechoVarDir = $typechoRoot . '/var';
$sentinel = $typechoVarDir . '/Typecho/Common.php';
if (!defined('__TYPECHO_ROOT_DIR__')) {
define('__TYPECHO_ROOT_DIR__', $typechoRoot);
}
if (!is_file($sentinel)) {
if (!is_dir(__DIR__ . '/.typecho')) {
mkdir(__DIR__ . '/.typecho', 0777, true);
}
$command = sprintf(
'git clone --depth 1 --branch %s https://github.com/typecho/typecho.git %s 2>&1',
escapeshellarg($typechoVersion),
escapeshellarg($typechoRoot)
);
$output = [];
$exitCode = 0;
exec($command, $output, $exitCode);
if ($exitCode !== 0 || !is_file($sentinel)) {
fwrite(STDERR, "Failed to clone Typecho {$typechoVersion}.\n");
fwrite(STDERR, "Command: {$command}\n");
fwrite(STDERR, implode("\n", $output) . "\n");
fwrite(STDERR, "You can manually clone Typecho into tests/.typecho/{$typechoVersion}/\n");
exit(1);
}
}
spl_autoload_register(static function (string $class) use ($typechoVarDir): void {
if (
!str_starts_with($class, 'Typecho\\')
&& !str_starts_with($class, 'Widget\\')
&& !str_starts_with($class, 'IXR\\')
) {
return;
}
$relative = str_replace('\\', '/', $class) . '.php';
$path = $typechoVarDir . '/' . $relative;
if (is_file($path)) {
require_once $path;
}
});
$required = [
'Typecho\\Plugin\\PluginInterface',
'Typecho\\Widget\\Helper\\Form',
'Widget\\Options',
'Typecho\\Plugin',
];
foreach ($required as $name) {
if (!class_exists($name) && !interface_exists($name)) {
throw new RuntimeException(
"Required Typecho symbol not found after autoload: {$name} (TYPECHO_VERSION={$typechoVersion})"
);
}
}
require_once __DIR__ . '/../MarkdownParse.php';
require_once __DIR__ . '/../Plugin.php';
================================================
FILE: tests/docker-compose.yml
================================================
services:
typecho:
image: joyqi/typecho:nightly-php8.4-apache
container_name: TYPECHO
restart: always
environment:
- TIMEZONE=Asia/Shanghai
- TYPECHO_INSTALL=1
- TYPECHO_DB_ADAPTER=Pdo_SQLite
- TYPECHO_DB_FILE=/app/usr/uploads/db.sqlite
- TYPECHO_SITE_URL=http://127.0.0.1:8080
- TYPECHO_USER_NAME=typecho
- TYPECHO_USER_PASSWORD=V40yx1iqybZgOWsxq6dtEed45Z1Vr1zl
- TYPECHO_USER_MAIL=typecho@localhost.local
ports:
- 8080:80
volumes:
- typecho-data:/app/usr
- ../:/app/usr/plugins/MarkdownParse
volumes:
typecho-data:
================================================
FILE: tests/fixtures/.gitkeep
================================================
================================================
FILE: tests/fixtures/mathjax-mixed.md
================================================
inline math: $a^2 + b^2 = c^2$
block math:
$$
\int_0^1 x \, dx = \frac{1}{2}
$$
================================================
FILE: tests/fixtures/mermaid-flowchart.md
================================================
# Diagram
```mermaid
graph TD
A --> B
```
================================================
FILE: tests/fixtures/toc-multi-level.md
================================================
[TOC]
# Chapter 1
## Section 1.1
# Chapter 2
gitextract_utg7yo0b/
├── .github/
│ └── workflows/
│ ├── auto-release.yml
│ └── pr-check.yml
├── .gitignore
├── LICENSE.md
├── MarkdownParse.php
├── Plugin.php
├── README.md
├── composer.json
├── phpunit.xml.dist
├── renovate.json
└── tests/
├── Support/
│ ├── Fixtures.php
│ └── ResetSingletonsTrait.php
├── Unit/
│ ├── BootstrapSanityTest.php
│ ├── MarkdownParseTest.php
│ ├── PluginRequireTest.php
│ ├── PluginTest.php
│ ├── SmokeTest.php
│ └── Support/
│ └── FixturesTest.php
├── bootstrap.php
├── docker-compose.yml
└── fixtures/
├── .gitkeep
├── mathjax-mixed.md
├── mermaid-flowchart.md
└── toc-multi-level.md
SYMBOL INDEX (75 symbols across 10 files)
FILE: MarkdownParse.php
class MarkdownParse (line 29) | class MarkdownParse
method __construct (line 48) | private function __construct()
method getInstance (line 58) | public static function getInstance(): MarkdownParse
method parse (line 78) | public function parse(string $text, array $config = []): string
method preParse (line 100) | public function preParse(string $text, array $config = []): array
method postParse (line 132) | public function postParse(string $htmlContent, array $config = []): array
method getConfig (line 152) | public function getConfig(): array
method addCommonMarkExtensions (line 189) | private function addCommonMarkExtensions(Environment $environment): void
method getIsTocEnable (line 212) | public function getIsTocEnable(): bool
method setIsTocEnable (line 222) | public function setIsTocEnable(bool $isTocEnable): void
method getIsNeedMermaid (line 232) | public function getIsNeedMermaid(): bool
method setIsNeedMermaid (line 242) | public function setIsNeedMermaid(bool $isNeedMermaid): void
method getIsNeedLaTex (line 252) | public function getIsNeedLaTex(): bool
method setIsNeedLaTex (line 262) | public function setIsNeedLaTex(bool $isNeedLaTex): void
method getInternalHosts (line 272) | public function getInternalHosts(): string
method setInternalHosts (line 282) | public function setInternalHosts(string $internalHosts): void
FILE: Plugin.php
class Plugin (line 21) | class Plugin implements PluginInterface
method activate (line 39) | public static function activate()
method deactivate (line 46) | public static function deactivate()
method config (line 51) | public static function config(Form $form)
method personalConfig (line 75) | public static function personalConfig(Form $form)
method parse (line 80) | public static function parse($text)
method resourceLink (line 90) | public static function resourceLink()
FILE: tests/Support/Fixtures.php
class Fixtures (line 9) | final class Fixtures
method load (line 11) | public static function load(string $name): string
FILE: tests/Support/ResetSingletonsTrait.php
type ResetSingletonsTrait (line 13) | trait ResetSingletonsTrait
method resetMarkdownParse (line 15) | protected function resetMarkdownParse(): void
method resetTypechoWidgets (line 22) | protected function resetTypechoWidgets(): void
FILE: tests/Unit/BootstrapSanityTest.php
class BootstrapSanityTest (line 10) | final class BootstrapSanityTest extends TestCase
method testTypechoCoreSymbolsAreLoaded (line 12) | public function testTypechoCoreSymbolsAreLoaded(): void
method testMarkdownParseClassIsLoaded (line 20) | public function testMarkdownParseClassIsLoaded(): void
FILE: tests/Unit/MarkdownParseTest.php
class MarkdownParseTest (line 12) | final class MarkdownParseTest extends TestCase
method setUp (line 18) | protected function setUp(): void
method testParsesAtxHeading (line 25) | public function testParsesAtxHeading(): void
method testParsesUnorderedList (line 32) | public function testParsesUnorderedList(): void
method testParsesFencedCodeBlock (line 40) | public function testParsesFencedCodeBlock(): void
method testParsesGfmTable (line 47) | public function testParsesGfmTable(): void
method testParsesTaskList (line 56) | public function testParsesTaskList(): void
method testParsesStrikethrough (line 64) | public function testParsesStrikethrough(): void
method testParsesFootnote (line 70) | public function testParsesFootnote(): void
method testParsesDescriptionList (line 77) | public function testParsesDescriptionList(): void
method testMermaidCodeBlockGetsClassMermaid (line 86) | public function testMermaidCodeBlockGetsClassMermaid(): void
method testNonMermaidFencedBlockUntouched (line 96) | public function testNonMermaidFencedBlockUntouched(): void
method testInlineMathMarksLatexNeeded (line 105) | public function testInlineMathMarksLatexNeeded(): void
method testBlockMathPreParseWrappingIsRemovedInOutput (line 111) | public function testBlockMathPreParseWrappingIsRemovedInOutput(): void
method testBacktickInlineCodeDoesNotTriggerLatex (line 122) | public function testBacktickInlineCodeDoesNotTriggerLatex(): void
method testTocRendersWhenEnabled (line 136) | public function testTocRendersWhenEnabled(): void
method testTocNotRenderedWhenDisabled (line 147) | public function testTocNotRenderedWhenDisabled(): void
method testExternalLinkGetsRelAttributes (line 154) | public function testExternalLinkGetsRelAttributes(): void
method testInternalLinkDoesNotOpenInNewWindow (line 166) | public function testInternalLinkDoesNotOpenInNewWindow(): void
method testInternalHostsAcceptsRegex (line 175) | public function testInternalHostsAcceptsRegex(): void
method testImageGetsLazyLoading (line 188) | public function testImageGetsLazyLoading(): void
method testMarkSyntaxRendersMarkTag (line 196) | public function testMarkSyntaxRendersMarkTag(): void
method testHeadingHasPermalinkAnchor (line 202) | public function testHeadingHasPermalinkAnchor(): void
FILE: tests/Unit/PluginRequireTest.php
class PluginRequireTest (line 12) | final class PluginRequireTest extends TestCase
method testPluginClassIsLoadable (line 16) | public function testPluginClassIsLoadable(): void
method testResetMarkdownParseGivesFreshInstance (line 21) | public function testResetMarkdownParseGivesFreshInstance(): void
method testResetTypechoWidgetsDoesNotThrow (line 33) | public function testResetTypechoWidgetsDoesNotThrow(): void
FILE: tests/Unit/PluginTest.php
class PluginTest (line 14) | final class PluginTest extends TestCase
method setUp (line 19) | protected function setUp(): void
method mockOptions (line 30) | private function mockOptions(array $pluginOptions, string $siteUrl = '...
method testParseEnablesTocWhenConfigEnabled (line 50) | public function testParseEnablesTocWhenConfigEnabled(): void
method testParseFallsBackToSiteUrlHostWhenInternalHostsEmpty (line 64) | public function testParseFallsBackToSiteUrlHostWhenInternalHostsEmpty(...
method testResourceLinkEmitsMermaidWhenForceEnabled (line 77) | public function testResourceLinkEmitsMermaidWhenForceEnabled(): void
method testResourceLinkEmitsMermaidOnAutoWhenContentNeedsIt (line 97) | public function testResourceLinkEmitsMermaidOnAutoWhenContentNeedsIt()...
method testResourceLinkOmitsMermaidWhenDisabled (line 119) | public function testResourceLinkOmitsMermaidWhenDisabled(): void
method testResourceLinkEmitsMathjaxOnAutoWhenInlineMathPresent (line 139) | public function testResourceLinkEmitsMathjaxOnAutoWhenInlineMathPresen...
method testResourceLinkEmitsNothingWhenAllDisabled (line 160) | public function testResourceLinkEmitsNothingWhenAllDisabled(): void
method testConfigDoesNotThrow (line 178) | public function testConfigDoesNotThrow(): void
method testActivateDoesNotThrow (line 188) | public function testActivateDoesNotThrow(): void
FILE: tests/Unit/SmokeTest.php
class SmokeTest (line 9) | final class SmokeTest extends TestCase
method testPhpUnitIsWired (line 11) | public function testPhpUnitIsWired(): void
FILE: tests/Unit/Support/FixturesTest.php
class FixturesTest (line 11) | final class FixturesTest extends TestCase
method testLoadsExistingFile (line 13) | public function testLoadsExistingFile(): void
method testThrowsWhenFixtureMissing (line 27) | public function testThrowsWhenFixtureMissing(): void
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
{
"path": ".github/workflows/auto-release.yml",
"chars": 4768,
"preview": "name: Auto Release\n\non:\n push:\n branches: [master]\n\njobs:\n release:\n runs-on: ubuntu-latest\n if: \"!contains(g"
},
{
"path": ".github/workflows/pr-check.yml",
"chars": 1654,
"preview": "name: Pull Request Check\n\non:\n pull_request:\n branches: [ main, master ]\n types: [ opened, synchronize, reopened "
},
{
"path": ".gitignore",
"chars": 87,
"preview": "vendor\nvendor.phar\ndocs\n.worktrees\ntests/.typecho\n.phpunit.cache\n.phpunit.result.cache\n"
},
{
"path": "LICENSE.md",
"chars": 1076,
"preview": "The MIT License (MIT)\n\nCopyright (c) Taylor Otwell\n\nPermission is hereby granted, free of charge, to any person obtainin"
},
{
"path": "MarkdownParse.php",
"chars": 9754,
"preview": "<?php\n\nnamespace TypechoPlugin\\MarkdownParse;\n\nrequire_once __DIR__ . '/vendor/autoload.php';\n\nuse League\\CommonMark\\Env"
},
{
"path": "Plugin.php",
"chars": 6443,
"preview": "<?php\n\nnamespace TypechoPlugin\\MarkdownParse;\n\nif (file_exists(__DIR__ . '/vendor.phar')) {\n require_once 'phar://' ."
},
{
"path": "README.md",
"chars": 3637,
"preview": "Markdown Plugin for Typecho\n=========================\n\n[;\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Support;\n\nuse RuntimeException;\n\nfinal clas"
},
{
"path": "tests/Support/ResetSingletonsTrait.php",
"chars": 1222,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Support;\n\nuse ReflectionClass;\nuse Reflecti"
},
{
"path": "tests/Unit/BootstrapSanityTest.php",
"chars": 801,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Unit;\n\nuse PHPUnit\\Framework\\TestCase;\nuse "
},
{
"path": "tests/Unit/MarkdownParseTest.php",
"chars": 7757,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Unit;\n\nuse PHPUnit\\Framework\\TestCase;\nuse "
},
{
"path": "tests/Unit/PluginRequireTest.php",
"chars": 994,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Unit;\n\nuse PHPUnit\\Framework\\TestCase;\nuse "
},
{
"path": "tests/Unit/PluginTest.php",
"chars": 6446,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Unit;\n\nuse Mockery;\nuse Mockery\\Adapter\\Php"
},
{
"path": "tests/Unit/SmokeTest.php",
"chars": 252,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Unit;\n\nuse PHPUnit\\Framework\\TestCase;\n\nfin"
},
{
"path": "tests/Unit/Support/FixturesTest.php",
"chars": 894,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nnamespace TypechoPlugin\\MarkdownParse\\Tests\\Unit\\Support;\n\nuse PHPUnit\\Framework\\TestCa"
},
{
"path": "tests/bootstrap.php",
"chars": 2054,
"preview": "<?php\n\ndeclare(strict_types=1);\n\nrequire_once __DIR__ . '/../vendor/autoload.php';\n\n$typechoVersion = getenv('TYPECHO_VE"
},
{
"path": "tests/docker-compose.yml",
"chars": 612,
"preview": "services:\n typecho:\n image: joyqi/typecho:nightly-php8.4-apache\n container_name: TYPECHO\n restart: always\n "
},
{
"path": "tests/fixtures/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "tests/fixtures/mathjax-mixed.md",
"chars": 82,
"preview": "inline math: $a^2 + b^2 = c^2$\n\nblock math:\n\n$$\n\\int_0^1 x \\, dx = \\frac{1}{2}\n$$\n"
},
{
"path": "tests/fixtures/mermaid-flowchart.md",
"chars": 45,
"preview": "# Diagram\n\n```mermaid\ngraph TD\n A --> B\n```\n"
},
{
"path": "tests/fixtures/toc-multi-level.md",
"chars": 48,
"preview": "[TOC]\n\n# Chapter 1\n\n## Section 1.1\n\n# Chapter 2\n"
}
]
About this extraction
This page contains the full source code of the mrgeneralgoo/typecho-markdown GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (49.5 KB), approximately 13.6k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.