在 DokuWiki 中,语法插件的 getType()
方法返回值有以下几种类型:
container
- 容器类型,可以包含其他模式baseonly
- 只允许在基本模式中使用的类型formatting
- 文本格式化类型substition
- 简单的标记替换类型protected
- 内容按原样保留的类型disabled
- 无 wiki 标记但不保留空白的类型paragraphs
- 段落边界标记类型
这些类型在 DokuWiki 的解析器系统中用于确定语法插件如何与其他语法元素交互。
详细解释
在 DokuWiki 中,每个语法插件都必须继承 SyntaxPlugin
类并实现 getType()
方法。 SyntaxPlugin.php:18-29
getType()
方法需要返回 Parser.php 中定义的 $PARSER_MODES
中的一种模式类型。这些类型决定了插件在解析过程中的行为方式。
根据 Parser and Renderer 的 wiki 页面,这些模式类型分为几个类别:
- Container(容器):复杂的模式,可以包含其他模式,例如 listblock、table、quote、hr
- Base-only(仅基本):只允许在基本模式中使用的模式,例如 header
- Formatting(格式化):文本样式模式,例如 strong、emphasis、underline 等
- Substitution(替换):简单的标记替换模式,例如 internallink、media、externallink 等
- Protected(受保护):内容按原样保留,例如 preformatted、code、file
- Disabled(禁用):无 wiki 标记但不保留空白,例如 unformatted
- Paragraphs(段落):段落边界标记,例如 eol
这些类型在解析器系统中用于确定语法插件如何与其他语法元素交互,以及它们在文档结构中的位置。
在解析过程中,DokuWiki 会根据语法插件的类型来决定如何处理它们。例如,在 p_get_parsermodes()
函数中,会收集所有语法模式及其对象,然后按照正确的顺序对它们进行排序并添加到解析器中。 parserutils.php:555-587
当语法插件被加载时,它的 getType()
方法返回的值会被注册到 $PARSER_MODES
数组中,这样解析器就知道如何处理该插件。 parserutils.php:580
Notes
这个回答基于 DokuWiki 的代码库,特别是 SyntaxPlugin 类和解析器系统的实现。getType()
方法是每个语法插件必须实现的方法,它返回的值决定了插件在解析过程中的行为方式。
1. container
类型插件完整实现
容器类型的插件可以包含其他模式,例如表格、列表等。
<?php
/**
* 表格语法插件示例 - 容器类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
// 必须是这个格式的类名: syntax_plugin_插件名称
class syntax_plugin_example_table extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'container';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'block';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return ['formatting', 'substition', 'disabled', 'protected'];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 50;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addEntryPattern('<table>(?=.*?</table>)', $mode, 'plugin_example_table');
}
public function postConnect() {
$this->Lexer->addExitPattern('</table>', 'plugin_example_table');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
switch ($state) {
case DOKU_LEXER_ENTER:
return [
'state' => $state,
'pos' => $pos
];
case DOKU_LEXER_UNMATCHED:
return [
'state' => $state,
'text' => $match,
'pos' => $pos
];
case DOKU_LEXER_EXIT:
return [
'state' => $state,
'pos' => $pos
];
}
return false;
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
switch ($data['state']) {
case DOKU_LEXER_ENTER:
$renderer->doc .= '<div class="example_table">';
break;
case DOKU_LEXER_UNMATCHED:
$renderer->doc .= $renderer->_xmlEntities($data['text']);
break;
case DOKU_LEXER_EXIT:
$renderer->doc .= '</div>';
break;
}
return true;
}
return false;
}
}
2. baseonly
类型插件完整实现
只允许在基本模式中使用的类型,例如标题。
<?php
/**
* 标题语法插件示例 - 仅基本类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
class syntax_plugin_example_header extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'baseonly';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'block';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return [];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 10;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addSpecialPattern('={3,}[^\n]+={3,}', $mode, 'plugin_example_header');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
// 去除等号并修剪空白
$title = trim(preg_replace('/^={3,}(.+)={3,}$/', '$1', $match));
return [
'title' => $title,
'level' => 3, // 假设这是一个 h3 标题
'pos' => $pos
];
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
$renderer->header($data['title'], $data['level'], $data['pos']);
return true;
}
return false;
}
}
3. formatting
类型插件完整实现
文本格式化类型的插件,用于文本样式,如高亮等。
<?php
/**
* 文本高亮语法插件示例 - 格式化类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
class syntax_plugin_example_highlight extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'formatting';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'normal';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return ['formatting', 'substition'];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 150;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addEntryPattern('%%(?=.*?%%)', $mode, 'plugin_example_highlight');
}
public function postConnect() {
$this->Lexer->addExitPattern('%%', 'plugin_example_highlight');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
switch ($state) {
case DOKU_LEXER_ENTER:
return [
'state' => $state,
'pos' => $pos
];
case DOKU_LEXER_UNMATCHED:
return [
'state' => $state,
'text' => $match,
'pos' => $pos
];
case DOKU_LEXER_EXIT:
return [
'state' => $state,
'pos' => $pos
];
}
return false;
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
switch ($data['state']) {
case DOKU_LEXER_ENTER:
$renderer->doc .= '<span class="highlight">';
break;
case DOKU_LEXER_UNMATCHED:
$renderer->doc .= $renderer->_xmlEntities($data['text']);
break;
case DOKU_LEXER_EXIT:
$renderer->doc .= '</span>';
break;
}
return true;
}
return false;
}
}
4. substition
类型插件完整实现
简单的标记替换类型,例如特殊链接。
<?php
/**
* 特殊链接语法插件示例 - 替换类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
class syntax_plugin_example_speciallink extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'substition';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'normal';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return [];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 300;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addSpecialPattern('\[\[special>.+?\]\]', $mode, 'plugin_example_speciallink');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
// 提取链接文本
$link = trim(substr($match, 11, -2)); // 去除 [[special> 和 ]]
// 分割链接和标题(如果有)
$parts = explode('|', $link, 2);
$url = trim($parts[0]);
$title = isset($parts[1]) ? trim($parts[1]) : $url;
return [
'url' => $url,
'title' => $title,
'pos' => $pos
];
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
$renderer->doc .= '<a href="' . $renderer->_xmlEntities($data['url']) . '" class="speciallink">' .
$renderer->_xmlEntities($data['title']) . '</a>';
return true;
}
return false;
}
}
5. protected
类型插件完整实现
内容按原样保留的类型,例如代码块。
<?php
/**
* 代码块语法插件示例 - 受保护类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
class syntax_plugin_example_codeblock extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'protected';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'block';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return [];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 200;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addEntryPattern('<code\b.*?>(?=.*?</code>)', $mode, 'plugin_example_codeblock');
}
public function postConnect() {
$this->Lexer->addExitPattern('</code>', 'plugin_example_codeblock');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
switch ($state) {
case DOKU_LEXER_ENTER:
// 提取语言(如果有)
$language = '';
if (preg_match('/<code\b(.*?)>/', $match, $matches)) {
if (!empty($matches[1])) {
$language = trim($matches[1]);
}
}
return [
'state' => $state,
'language' => $language,
'pos' => $pos
];
case DOKU_LEXER_UNMATCHED:
return [
'state' => $state,
'text' => $match,
'pos' => $pos
];
case DOKU_LEXER_EXIT:
return [
'state' => $state,
'pos' => $pos
];
}
return false;
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
switch ($data['state']) {
case DOKU_LEXER_ENTER:
$class = !empty($data['language']) ? ' class="' . $renderer->_xmlEntities($data['language']) . '"' : '';
$renderer->doc .= '<pre' . $class . '><code>';
break;
case DOKU_LEXER_UNMATCHED:
$renderer->doc .= $renderer->_xmlEntities($data['text']);
break;
case DOKU_LEXER_EXIT:
$renderer->doc .= '</code></pre>';
break;
}
return true;
}
return false;
}
}
6. disabled
类型插件完整实现
无 wiki 标记但不保留空白的类型。
<?php
/**
* 无格式文本语法插件示例 - 禁用类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
class syntax_plugin_example_plaintext extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'disabled';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'normal';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return [];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 250;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addEntryPattern('<nowiki>(?=.*?</nowiki>)', $mode, 'plugin_example_plaintext');
}
public function postConnect() {
$this->Lexer->addExitPattern('</nowiki>', 'plugin_example_plaintext');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
switch ($state) {
case DOKU_LEXER_ENTER:
return [
'state' => $state,
'pos' => $pos
];
case DOKU_LEXER_UNMATCHED:
return [
'state' => $state,
'text' => $match,
'pos' => $pos
];
case DOKU_LEXER_EXIT:
return [
'state' => $state,
'pos' => $pos
];
}
return false;
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
switch ($data['state']) {
case DOKU_LEXER_ENTER:
$renderer->doc .= '<span class="nowiki">';
break;
case DOKU_LEXER_UNMATCHED:
// 在禁用模式中,我们保留文本但不保留空白格式
$text = $data['text'];
// 将多个空白字符替换为单个空格
$text = preg_replace('/\s+/', ' ', $text);
$renderer->doc .= $renderer->_xmlEntities($text);
break;
case DOKU_LEXER_EXIT:
$renderer->doc .= '</span>';
break;
}
return true;
}
return false;
}
}
7. paragraphs
类型插件完整实现
段落边界标记类型。
<?php
/**
* 段落处理语法插件示例 - 段落类型
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
*/
class syntax_plugin_example_paragraph extends \dokuwiki\Extension\SyntaxPlugin {
/**
* 返回插件类型
*/
public function getType() {
return 'paragraphs';
}
/**
* 返回段落类型
*/
public function getPType() {
return 'block';
}
/**
* 返回允许的模式类型
*/
public function getAllowedTypes() {
return [];
}
/**
* 返回模式排序值
*/
public function getSort() {
return 400;
}
/**
* 将模式连接到词法分析器
*/
public function connectTo($mode) {
$this->Lexer->addSpecialPattern('\n{2,}', $mode, 'plugin_example_paragraph');
}
/**
* 处理匹配的数据
*/
public function handle($match, $state, $pos, \Doku_Handler $handler) {
return [
'count' => strlen($match), // 计算连续换行符的数量
'pos' => $pos
];
}
/**
* 创建输出
*/
public function render($format, \Doku_Renderer $renderer, $data) {
if ($format == 'xhtml') {
// 关闭当前段落并开始一个新段落
$renderer->p_close();
// 如果有多个换行符,可以添加额外的垂直空间
if ($data['count'] > 2) {
$renderer->doc .= '<div class="extra_spacing" style="margin-top: ' .
(($data['count'] - 2) * 0.5) . 'em;"></div>';
}
$renderer->p_open();
return true;
}
return false;
}
}
这些完整的实现示例展示了如何创建不同类型的 DokuWiki 语法插件。每种类型的插件都有其特定的行为和用途,通过 getType()
方法返回的值来定义。
disabled
类型的插件特别适用于需要显示原始文本但不需要保留空白格式的情况,例如 <nowiki>
标签。这种类型的插件会禁用所有 wiki 标记的解析,但不会像 protected
类型那样保留空白和换行。
Notes
这些示例代码展示了 DokuWiki 语法插件系统的灵活性和可扩展性。每种类型的插件都有其特定的用途和行为特征,开发者可以根据需要选择适当的类型。
在 DokuWiki 中,语法插件的类型(通过 getType()
返回)决定了它在解析过程中的行为方式,而 getAllowedTypes()
方法则决定了哪些其他语法元素可以在该插件内部使用。这两个方法共同构成了 DokuWiki 强大而灵活的语法系统。
在 DokuWiki 的语法插件系统中,getAllowedTypes()
方法用于定义该插件内部可以包含哪些其他类型的语法模式。
getAllowedTypes()
方法的作用
getAllowedTypes()
方法返回一个数组,包含该插件允许在其内部使用的其他语法模式类型。这些类型必须是 $PARSER_MODES
中定义的类型之一,如 'container'、'formatting'、'substition' 等。 SyntaxPlugin.php:31-39
例如,当一个插件返回 ['formatting', 'substition']
时,这意味着该插件内部可以包含格式化类型(如粗体、斜体)和替换类型(如链接、媒体)的语法元素,但不能包含其他类型(如容器类型)的元素。
返回多个值的意义
返回多个值(数组)意味着该插件允许在其内部使用多种不同类型的语法模式。这对于创建复杂的、可嵌套的语法结构非常重要。
例如,在您之前看到的容器类型插件示例中:
public function getAllowedTypes() {
return ['formatting', 'substition', 'disabled', 'protected'];
}
这表示该表格插件允许在其内部使用:
- 格式化类型(如粗体、斜体)
- 替换类型(如链接、媒体)
- 禁用类型(如无格式文本)
- 受保护类型(如代码块)
但它不允许在其内部使用容器类型(如嵌套表格)或仅基本类型(如标题)。
实际工作原理
当 DokuWiki 解析文档时,它会根据插件的 getAllowedTypes()
方法返回的类型来决定哪些语法可以在该插件内部使用。这是通过 accepts()
方法实现的: SyntaxPlugin.php:111-129
这段代码展示了 DokuWiki 如何使用 getAllowedTypes()
方法的返回值来构建允许的模式列表。它首先获取允许的模式类型,然后从全局 $PARSER_MODES
数组中获取这些类型对应的所有具体模式。
例如,如果 getAllowedTypes()
返回 ['formatting']
,那么 $this->allowedModes
将包含所有格式化类型的模式,如 'strong'、'emphasis'、'underline' 等。
与 getType()
的区别
需要注意的是,getAllowedTypes()
与 getType()
是两个不同的方法:
getType()
定义了插件本身的类型(如 'container'、'formatting' 等)getAllowedTypes()
定义了插件内部允许使用的其他语法类型
这两个方法共同决定了插件在 DokuWiki 语法系统中的行为和与其他语法元素的交互方式。
Notes
getAllowedTypes()
方法是 DokuWiki 语法插件系统中的一个重要部分,它允许插件定义自己的"语法作用域",即哪些其他语法元素可以在该插件内部使用。这对于创建复杂的、可嵌套的语法结构非常重要,同时也确保了语法的一致性和可预测性。
默认情况下,SyntaxPlugin
类的 getAllowedTypes()
方法返回空数组,表示不允许任何其他语法模式在其内部使用。插件开发者需要根据自己的需求覆盖这个方法。
Dokuwiki 语法插件的迷思
addEntryPattern 和 addExitPattern
当你实现容器的两头匹配时,DOKU_LEXER_UNMATCHED 不做任何处理,那么 getAllowedTypes() 返回的子语法类型会被系统接管接着解析。
但如果你还添加了 addPattern 用于匹配中间的内容,那么又想让中间匹配的内容实现语法嵌套,这跟 getAllowedTypes() 就没什么关系了,直接 p_get_instructions 和 next 来手动解析和渲染内容。
以下是一个完整的容器语法插件示例,支持内嵌语法:
<?php
/**
* Plugin example: 一个支持内嵌语法的容器插件
*/
if (!defined('DOKU_INC')) die();
class syntax_plugin_example extends \dokuwiki\Extension\SyntaxPlugin {
public function getType() {
return 'container';
}
public function getPType() {
return 'block';
}
public function getSort() {
return 59;
}
// 允许内嵌的语法类型
public function getAllowedTypes() {
return ['formatting', 'substition', 'disabled', 'protected'];
}
public function connectTo($mode) {
$this->Lexer->addEntryPattern(
'<example>',
$mode,
'plugin_example'
);
}
public function postConnect() {
$this->Lexer->addExitPattern(
'</example>',
'plugin_example'
);
}
public function handle($match, $state, $pos, Doku_Handler $handler) {
switch ($state) {
case DOKU_LEXER_ENTER:
return ['state' => $state];
case DOKU_LEXER_UNMATCHED:
// 直接返回内容,让系统处理内嵌语法
return ['state' => $state, 'text' => $match];
case DOKU_LEXER_EXIT:
return ['state' => $state];
}
return false;
}
public function render($mode, Doku_Renderer $renderer, $data) {
if ($mode !== 'xhtml') return false;
switch ($data['state']) {
case DOKU_LEXER_ENTER:
$renderer->doc .= "<div class='example'>\n";
break;
case DOKU_LEXER_UNMATCHED:
// 系统已经处理了内嵌语法,不需要特殊处理
break;
case DOKU_LEXER_EXIT:
$renderer->doc .= "</div>\n";
break;
}
return true;
}
}
工作原理
- 当 Lexer 遇到匹配
<example>
的文本时,它会进入plugin_example
模式,并调用handle()
方法,传入DOKU_LEXER_ENTER
状态。 - 在
plugin_example
模式下,Lexer 会继续解析文本,直到遇到匹配</example>
的文本,此时它会退出该模式,并调用handle()
方法,传入DOKU_LEXER_EXIT
状态。 - 在这两个状态之间,Lexer 会将未匹配的文本传递给
handle()
方法,状态为DOKU_LEXER_UNMATCHED
。 - 由于您实现了
getAllowedTypes()
方法,DokuWiki 的解析系统会自动处理 UNMATCHED 内容中的嵌入语法。
Notes
在 DokuWiki 中,处理容器语法并支持内嵌语法非常简单。您只需要:
- 实现
getAllowedTypes()
方法,声明允许的内嵌语法类型 - 在
handle()
方法中,对于 UNMATCHED 状态,直接返回未处理的内容 - 在
render()
方法中,不需要特殊处理内嵌语法,系统会自动处理
这种方法比手动使用 p_get_instructions
和 nest
方法更简单,因为您不需要自己管理解析和渲染过程,而是依赖 DokuWiki 的自动处理机制。
情况一:容器语法的两头匹配
当您实现一个容器语法,只处理开头和结尾的匹配(使用 addEntryPattern
和 addExitPattern
)时:
- 开头匹配会触发
DOKU_LEXER_ENTER
状态 - 结尾匹配会触发
DOKU_LEXER_EXIT
状态 - 中间的内容会以
DOKU_LEXER_UNMATCHED
状态传递给您的handle()
方法
如果您在 handle()
方法中对 DOKU_LEXER_UNMATCHED
状态的内容不做任何特殊处理,只是简单地返回:
case DOKU_LEXER_UNMATCHED:
return ['state' => $state, 'text' => $match];
那么 DokuWiki 会根据您在 getAllowedTypes()
中声明的语法类型,自动处理这些内容中的嵌套语法。
这是通过 Doku_Handler
类的 plugin()
方法实现的: handler.php:292-304
情况二:使用 addPattern 匹配中间内容
如果您还使用了 addPattern
来匹配容器中间的特定内容,这些内容会以 DOKU_LEXER_MATCHED
状态传递给您的 handle()
方法。
在这种情况下,如果您想让这些匹配的内容也支持嵌套语法,那么 getAllowedTypes()
确实不再起作用,因为这些内容已经被您的模式匹配了,不会被视为 DOKU_LEXER_UNMATCHED
。
此时,您需要手动使用 p_get_instructions
和 nest
来解析和渲染这些内容:
case DOKU_LEXER_MATCHED:
// 手动处理匹配内容中的嵌套语法
return ['state' => $state, 'data' => $match];
然后在 render()
方法中:
case DOKU_LEXER_MATCHED:
$instructions = p_get_instructions($data['data']);
$renderer->nest($instructions);
break;
p_get_instructions
函数会创建一个全新的解析上下文: parserutils.php:219-234
而 nest
方法会执行这些解析指令: renderer.php:136-144
总结
- 容器两头匹配:
- 使用
addEntryPattern
和addExitPattern
- 实现
getAllowedTypes()
- 在
DOKU_LEXER_UNMATCHED
状态下简单返回内容 - 系统自动处理嵌套语法
- 使用
- 中间内容匹配:
- 使用
addPattern
匹配特定内容 - 在
DOKU_LEXER_MATCHED
状态下获取匹配内容 - 使用
p_get_instructions
和nest
手动处理嵌套语法 - 与
getAllowedTypes()
无关
- 使用
这两种方法可以在同一个插件中共存,用于处理不同类型的内容。
Notes
在 DokuWiki 的语法插件系统中,理解状态机制和解析流程非常重要。DOKU_LEXER_UNMATCHED
状态是系统自动处理嵌套语法的入口点,而 DOKU_LEXER_MATCHED
状态则需要您手动处理嵌套语法。这两种方法各有优缺点,可以根据具体需求选择使用。