hyvor / phrosemirror
PHP中的Prosemirror
Requires
- php: ^8.1
- ext-dom: *
- ext-libxml: *
- myclabs/deep-copy: ^1.11
Requires (Dev)
- pestphp/pest: ^2.0
- phpstan/phpstan: ^1.8
This package is auto-updated.
Last update: 2024-08-28 17:19:19 UTC
README
Phrosemirror 是一个 PHP 库,用于以简单且类型安全的方式处理 Prosemirror(或 TipTap)的 JSON 内容。
此库可以完成以下操作
- 将 Prosemirror JSON 转换为具有类型化节点、标记和属性的文档
- 分析和更改文档
- 将文档转换为 HTML
- 将文档转换为文本
- 将 HTML 解析为文档
- 使用
content
和group
以实现更严格的模式一致性
安装
composer require hyvor/phrosemirror
1. 模式
此库是无偏见的,这意味着没有默认的模式。为了开始,您必须定义与您的前端 Prosemirror 配置相似的方案。
您可以在本存储库的
/example
目录中找到一个示例方案,它与prosemirror-schema-basic
包的方案相似。
use Hyvor\Phrosemirror\Types\Schema; $schema = new Schema( [ new Doc, new Text, new Paragraph, new Blockquote, new Image ], [ new Strong, new Italic, ] );
在 Schema
构造函数中,第一个参数是一个节点类型的数组,第二个参数是标记类型的数组。
节点类型
一个基本的节点类型看起来像这样
use Hyvor\Phrosemirror\Types\NodeType; class Doc extends NodeType { public string $name = 'doc'; public ?string $content = 'block+'; }
它们可以包含 content
和 group
属性。如果未设置 content
,则不允许在此节点中添加内容。有关这些属性如何工作的更多信息,请参阅下面的 内容 & 组合。
这里还有一个节点类型的示例
class Paragraph extends NodeType { public string $name = 'paragraph'; public ?string $content = 'inline*'; public string $group = 'block'; }
标记类型
一个基本的标记类型看起来像这样
use Hyvor\Phrosemirror\Types\MarkType; class Strong extends MarkType { public string $name = 'strong'; }
属性(Attrs)
此库的主要目标之一是实现类型安全。因此,属性是在类型化的类中定义的。
use Hyvor\Phrosemirror\Types\AttrsType; class ImageAttrs extends AttrsType { public string $src; public ?string $alt; }
通过定义显式的类型,我们确保图像的
src
属性始终是字符串。alt
可以是字符串或 null。
您还可以为属性定义默认值,如果它们不在 JSON 文档中,则将使用这些默认值。
class ImageAttrs extends AttrsType { public string $src = 'https://hyvor.com/placeholder.png'; }
然后,在节点类型或标记类型中,您必须提到 Attrs 类。
use Hyvor\Phrosemirror\Types\NodeType; class Image extends NodeType { // ... public string $attrs = ImageAttrs::class; }
2. 文档
一旦方案准备就绪,我们就可以开始处理文档。
use Hyvor\Phrosemirror\Types\Schema; use Hyvor\Phrosemirror\Document\Document; $schema = new Schema($nodes, $marks); $json = '{}'; // <- this is the JSON from the front-end $document = Document::fromJson($schema, $json);
$json
可以是 JSON 字符串、PHP 数组或 PHP 对象。如果给定的 JSON 是有效的,则 $document
将是 Hyvor\Phrosemirror\Document\Document
的实例。如果无效,将抛出错误。有关错误处理的信息,请参阅以下内容。
节点
Document
只是一个具有 doc
类型的 Node
。以下是节点的属性。
namespace Hyvor\Phrosemirror\Document; use Hyvor\Phrosemirror\Types\NodeType; use Hyvor\Phrosemirror\Types\AttrsType; class Node { public NodeType $type; public AttrsType $attrs; public Fragment $content; public Mark[] $marks; }
NodeType $type
是节点的类型,您在方案中定义的类型
AttrsType $attrs
是节点的属性。这将是一个您在节点类型属性中定义的类的对象。例如,在上面的节点类型示例中,如果节点是 Image
,则 $attrs
将是 ImageAttrs
的对象。
Fragment $content
是子节点集合。
Mark[] $marks
是分配给此节点的标记数组。
TextNode
是一个特殊的Node
,它代表 Prosemirror 中的text
节点类型。它具有除上述属性外的string $text
属性。此外,$marks
仅在TextNode
的上下文中才有意义。
检查节点类型
使用 isOfType()
检查 Node
是否为您的方案中定义的特定 NodeType
。
$json = ['type' => 'paragraph']; $node = Node::fromJson($schema, $json); $node->isOfType(Paragraph::class); // true $node->isOfType(Image::class); // false $node->isOfType([Paragraph::class, Image::class]); // true
访问属性
使用attr()
方法访问节点属性。
$json = ['type' => 'image', 'attrs' => ['src' => 'image.png']]; $image = Node::fromJson($schema, $json); // html-escaped (safe to use in HTML output) $src = $image->attr('src'); // not html-escaped $src = $image->attr('src', escape: false);
遍历嵌套节点
您可以使用带有回调函数的traverse()
方法遍历嵌套节点。以下是一个遍历所有节点并找到所有图像节点的示例。
$document = Document::fromJson($schema, $json); $images = []; $document->traverse(function(Node $node) use(&$images) { if ($node->isOfType(Image::class)) { $images[] = $node; } })
traverse()
也会遍历TextNode
。
遍历直接子节点
使用foreach
和$node->content
。
foreach ($node->content as $child) { if ($child->isOfType(Image::class)) { echo "I found an image!"; } }
查找节点
之前,我们使用traverse()
来查找节点,但有一个更简单的getNodes()
方法。它搜索所有嵌套节点,并返回匹配节点的Node[]
数组。
// images $node->getNodes(Image::class); // all nodes (including TextNodes) $node->getNodes(); // nodes of multiple types $node->getNodes([Paragraph::class, Blockquote::class]); // images (only direct children) $node->getNodes(Image::class, false);
查找标记
类似于getNodes()
,您可以使用getMarks()
在当前节点内查找标记。它搜索所有嵌套节点,并返回匹配标记的Mark[]
数组。
// links $node->getMarks(Link::class); // all marks $node->getMarks(); // multiple types $node->getMarks([Strong::class, Italic::class]); // without nesting (marks of the current node only) $node->getMarks(Link::class, false);
JSON序列化
您可以将节点/文档序列化为JSON。
$node->toJson(); // JSON string $node->toArray(); // PHP array
标记
namespace Hyvor\Phrosemirror\Document; use Hyvor\Phrosemirror\Types\MarkType; use Hyvor\Phrosemirror\Types\AttrsType; class Mark { public MarkType $type; public AttrsType $attrs; }
$type
和$attrs
与节点的类似。
Mark
具有isOfType()
、attr()
、toArray()
和toJson()
方法,这些方法与Node
的方法类似。
$mark = Mark::fromJson(['type' => 'link', 'attrs' => ['src' => 'https://hyvor.com']); $mark->isOfType(Strong::class); // false $mark->attr('src'); // https://hyvor.com
片段
$node->content
是一个Fragment
。它包含一个子节点数组。您可以将其视为一个数组,但具有使事情更简单的辅助方法。
$fragment = $node->content(); // READ $fragment->first(); // Node | null $fragment->last(); // Node | null $fragment->nth(2); // Node | null $fragment->count(); // int // get all Nodes in the Fragment as an array $fragment->all(); // Node[] // loop through each node $fragment->each(fn (Node $node) => false); // WRITE (Be careful, these methods changes the document) $fragment->addNodeToStart($node); $fragment->addNodeToEnd($node); $fragment->addNode($node); // same as addNodeToEnd $fragment->setNodes($nodes); $fragment->map(fn (Node $node) => $node); // update nodes in a callback
3. HTML
接下来,让我们将您的文档转换为HTML。为此,您必须在节点类型和标记类型中定义toHtml()
方法。
use Hyvor\Phrosemirror\Document\Node; use Hyvor\Phrosemirror\Types\NodeType; class Paragraph extends NodeType { public function toHtml(Node $node, string $children) : string { return "<p>$children</p>"; } }
toHtml()
应返回节点的HTML字符串,并在其中放置$children
字符串。
以下是一个使用该节点属性的另一个示例。
use Hyvor\Phrosemirror\Document\Node; use Hyvor\Phrosemirror\Types\NodeType; class Image extends NodeType { public function toHtml(Node $node, string $children) : string { $src = $node->attr('src'); return "<img src=\"$src\">$children</p>"; } }
不要直接使用
$node->attrs->src
,因为原始属性未进行HTML转义。始终使用$node->attr()
或$node->attrs->get()
。
HTML:文档 -> HTML
使用toHtml()
方法将文档(或任何节点)序列化为HTML。
$document = Document::fromJson($schema, $json); $html = $document->toHtml();
解析HTML
HtmlParser
类负责将HTML解析为文档。它需要模式和某些解析规则来解析HTML。
<?php use Hyvor\Phrosemirror\Converters\HtmlParser\HtmlParser;use Hyvor\Phrosemirror\Converters\HtmlParser\ParserRule; $schema = new Schema($nodes, $marks); // this is the same schema you create for the document $parser = new HtmlParser($schema, [ new ParserRule(tag: 'p', node: 'paragraph'), new ParserRule(tag: '#text', node: 'text'), // ... other rules ]) $doc = $parser->parse($html);
然而,在大多数情况下,您只需要一个规则集来解析多个HTML输入。因此,您可以直接在Schema中定义规则(在节点和标记的fromHtml()
方法中)。
use Hyvor\Phrosemirror\Types\NodeType; use Hyvor\Phrosemirror\Converters\HtmlParser\ParserRule; use Hyvor\Phrosemirror\Document\Node; class Paragraph extends NodeType { public string $name = 'paragraph'; public ?string $content = 'inline*'; public string $group = 'block'; public function toHtml(Node $node, string $children): string { return "<p>$children</p>"; } public function fromHtml(): array { return [ new ParserRule(tag: 'p'), ]; } }
fromHtml()
方法应返回ParserRule[]
。在这里,不需要node
属性,因为它与节点类型的名称相同。
然后,使用fromSchema()
方法创建解析器。
$parser = HtmlParser::fromSchema($schema); $doc = $parser->parse($html);
解析HTML属性到节点属性
使用getAttrs()
方法从HTML元素中解析属性。
use DOMElement; class Image extends NodeType { public string $name = 'image'; public string $attrs = ImageAttrs::class; public function fromHtml() : array { return [ new ParserRule( tag: 'img', getAttrs: fn (DOMElement $element) => ImageAttrs::fromArray([ 'src' => $element->getAttribute('src'), 'alt' => $element->getAttribute('alt'), ]) ) ]; } }
getAttrs()
回调应返回以下之一:
false
以忽略元素null
如果找不到属性AttrsType
如果找到属性
内容 & 分组
在节点类型中定义content
和group
属性对于解析HTML很重要。
例如,假设我们有一个设置content
为block+
的blockquote
节点。这意味着blockquote
节点只能包含块节点。因此,以下HTML不符合模式。
<blockquote>Hello World</blockquote>
这时,content
和group
属性就派上用场了。因为我们知道blockquote
节点只能包含段落等块节点,HTML解析器将自动在解析时将文本包裹在段落中。生成的HTML将是
<blockquote><p>Hello World</p></blockquote>
这种逻辑由Sanitizer
类处理。简单地说,它会做以下事情以确保content
和group
的一致性
- 尝试包裹节点
- 尝试提升子节点
- 尝试连接内联节点
- 如果所有尝试都失败,它将删除节点
content
表达式支持Prosemirror前端库支持的所有内容。以下是一些示例
段落
段落|标题
段落?
段落*
段落+
段落{1,3}
段落{1,}
段落标题
(后续节点)段落(标题 | 代码块)+
block+
(使用分组)
注意:此清理过程仅在解析来自HTML的文档时运行。在解析来自JSON的文档时不会运行,因为我们期望JSON(通常来自您的前端)是有效的。但是,如果需要,您仍然可以按照以下方式运行清理过程
$doc = Document::fromJson($schema, $json); $sanitizedDoc = Sanitizer::sanitize($schema, $doc);
禁用清理
您可以在解析HTML时通过设置sanitize: false
来禁用content
清理。
$parser = HtmlParser::fromSchema($schema); $doc = $parser->parse($html, sanitize: false);
⚠️ 警告:禁用清理可能导致无效的文档。
错误处理
此库是严格的,并且它期望前端提供正确的输入。它可以抛出以下异常
InvalidJsonException
- 无效的JSONInvalidAttributeTypeException
- 无效的属性类型
这两个异常都扩展了PhrosemirrorException
类。因此,最佳实践是在构建文档时捕获它。
use Hyvor\Phrosemirror\Exception\PhrosemirrorException; try { $document = Document::fromJson($schema, $json); } catch (PhrosemirrorException $e) { // invalid document }
如果前端(JS)和后端(PHP)模式匹配,异常可能发生的情况仅当Prosemirror JSON被更改时。因此,停止处理此处是一个好的做法。
谁使用这个库?
- Hyvor Talk
- Hyvor Blogs
- 通过PR添加您的