hyvor/phrosemirror

PHP中的Prosemirror

1.0.4 2024-03-28 16:26 UTC

README

Phrosemirror 是一个 PHP 库,用于以简单且类型安全的方式处理 Prosemirror(或 TipTap)的 JSON 内容。

此库可以完成以下操作

  • 将 Prosemirror JSON 转换为具有类型化节点、标记和属性的文档
  • 分析和更改文档
  • 将文档转换为 HTML
  • 将文档转换为文本
  • 将 HTML 解析为文档
  • 使用 contentgroup 以实现更严格的模式一致性

安装

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+';
}

它们可以包含 contentgroup 属性。如果未设置 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如果找到属性

内容 & 分组

在节点类型中定义contentgroup属性对于解析HTML很重要。

例如,假设我们有一个设置contentblock+blockquote节点。这意味着blockquote节点只能包含块节点。因此,以下HTML不符合模式。

<blockquote>Hello World</blockquote>

这时,contentgroup属性就派上用场了。因为我们知道blockquote节点只能包含段落等块节点,HTML解析器将自动在解析时将文本包裹在段落中。生成的HTML将是

<blockquote><p>Hello World</p></blockquote>

这种逻辑由Sanitizer类处理。简单地说,它会做以下事情以确保contentgroup的一致性

  • 尝试包裹节点
  • 尝试提升子节点
  • 尝试连接内联节点
  • 如果所有尝试都失败,它将删除节点

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 - 无效的JSON
  • InvalidAttributeTypeException - 无效的属性类型

这两个异常都扩展了PhrosemirrorException类。因此,最佳实践是在构建文档时捕获它。

use Hyvor\Phrosemirror\Exception\PhrosemirrorException;

try {
    $document = Document::fromJson($schema, $json);
} catch (PhrosemirrorException $e) {
    // invalid document
}

如果前端(JS)和后端(PHP)模式匹配,异常可能发生的情况仅当Prosemirror JSON被更改时。因此,停止处理此处是一个好的做法。

谁使用这个库?