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 ================================================ 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
'], '', $htmlContent);
}
// If LaTeX is needed, remove tags added during preParse
if ($this->isNeedLaTex) {
$htmlContent = str_replace(['$$', '$$'], '$$', $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
================================================
'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('开启后支持解析并渲染 Mermaid'));
$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('可以去这里 实时编辑器调整主题配置看下效果'));
$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('开启后支持解析并渲染 MathJax'));
$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$/"),多个可用英文逗号分隔。
外部链接解析策略:默认在新窗口中打开,并加上 "noopener noreferrer" 属性'));
$form->addInput($elementInternalHosts);
$elementHelper = new Form\Element\Radio('show_help_info', [], self::RADIO_VALUE_DISABLE, _t('点击查看更新信息'), _t('点击查看语法手册'));
$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('', (string)Options::alloc()->plugin('MarkdownParse')->mermaid_theme ?: 'default');
}
if ($isAvailableMathjax) {
$resourceContent .= '';
$resourceContent .= '';
$resourceContent .= sprintf('', 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. **文本高亮:** 通过 `` 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 `` 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
================================================
tests/Unit
================================================
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
================================================
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
================================================
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
================================================
resetMarkdownParse();
$this->parser = MarkdownParse::getInstance();
}
public function testParsesAtxHeading(): void
{
$html = $this->parser->parse('# Hello');
$this->assertStringContainsString('assertStringContainsString('Hello', $html);
}
public function testParsesUnorderedList(): void
{
$html = $this->parser->parse("- a\n- b\n");
$this->assertStringContainsString('', $html);
$this->assertStringContainsString('- a
', $html);
$this->assertStringContainsString('- b
', $html);
}
public function testParsesFencedCodeBlock(): void
{
$html = $this->parser->parse("```php\necho 1;\n```\n");
$this->assertStringContainsString('
', $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('', $html);
$this->assertStringContainsString('a ', $html);
$this->assertStringContainsString('1 ', $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('gone', $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('', $html);
$this->assertStringContainsString('- term
', $html);
$this->assertStringContainsString('- definition
', $html);
}
public function testMermaidCodeBlockGetsClassMermaid(): void
{
$md = Fixtures::load('mermaid-flowchart.md');
$html = $this->parser->parse($md);
$this->assertStringContainsString('', $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('$$', $html);
$this->assertStringNotContainsString('$$', $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('', $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('
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('highlighted', $html);
}
public function testHeadingHasPermalinkAnchor(): void
{
$html = $this->parser->parse('# Hello world');
$this->assertStringContainsString('class="heading-permalink"', $html);
}
}
================================================
FILE: tests/Unit/PluginRequireTest.php
================================================
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
================================================
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('', $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('