dirtsimple/postmark

从Markdown文件同步WP内容

安装: 212

依赖: 0

建议者: 0

安全: 0

星星: 29

关注者: 3

分支: 7

开放问题: 1

类型:wp-cli-package

This package is auto-updated.

Last update: 2024-09-29 04:49:42 UTC


README

静态网站生成器允许您使用版本控制的Markdown文件树来创建站点,但提供不了很多主题或动态功能。Wordpress有很多主题和动态功能,但将内容锁定在数据库中的HTML中,您无法使用自己的编辑器或版本控制任何内容。

那么为什么不可以结合两者呢?

Postmark是一个wp-cli命令,它接受一个Markdown文件(或整个文件树)并创建或更新Wordpress中的帖子、页面和其他数据库对象。一些关键特性包括

  • 不需要更改web服务器配置:可以从服务器上的任何目录同步文件,并且不需要由web服务器可写或可读。(但是,它们需要由运行wp-cli命令的用户可读!)
  • 使用GUID来识别数据库中的帖子或页面以同步文件,因此相同的文件可以应用于多个wordpress站点(例如dev/staging/prod,或品牌网站之间的通用页面),移动或重命名文件会更改其WP中的父级或别名,而不是创建新的页面或帖子。
  • 文件可以包含YAML前端内容来设置所有标准的Wordpress页面/帖子属性
  • 允许自定义帖子类型,并且插件可以使用动作和过滤器在同步期间支持自定义字段或其他WP数据。
  • 可以使用选项引用同步插件的特殊页面(如结账或“我的账户”)和主题的自定义CSS,并且可以使用选项HTML值同步插件的HTML选项
  • 原型和模板:除了它们的WP帖子类型外,文件还可以有一个原型,从中继承属性和可选的Twig模板,该模板可以使用来自文档前端内容的的数据生成额外的静态内容。
  • 只有在输入文件(或其原型/模板文件)的大小、时间戳、名称、位置或内容发生变化时(除非您使用--force),才会更新帖子或页面
  • 与几乎任何文件监视工具(如entr、gulp、modd、reflex、guard等)配合使用效果极佳,可以立即更新保存的编辑过的帖子,即使工具无法传递更改的文件名,也只更新实际更改的文件。
  • 使用league/commonmark将Markdown转换为具有表格属性扩展,并且您可以通过过滤器添加其他扩展。Markdown内容也可以包含短代码。(不过,您可能需要使用反斜杠转义相邻的开括号,以防止它们被视为Markdown脚注链接。独占一行的短代码开启器或关闭器将被放置在任何段落、div、表格等之外,它们可能打断、先于或跟随。)
  • 支持父级帖子或页面(文件之上的最近index.md成为其父级,递归地)
  • 别名默认为文件名(或index.md的目录名),除非有其他指定
  • 文章/页面标题默认为Markdown主体第一行,如果它是一个标题。

Postmark在哲学上与imposer类似,即同步始终是单向的(从文件系统到数据库),但不会覆盖由输入文件指定的任何数据库内容。因此,文章或页面中不在Markdown或YAML中(如评论)的部分不会受到同步的影响。

(Postmark与imposer的另一个相似之处在于,它是由mantle预先安装的。Mantle项目包括一个可选的文件监视守护进程,该守护进程检测项目content/目录中Markdown文件的变化,并将它们自动同步到您的数据库。)

内容

概述

安装和使用

Postmark可以通过以下方式安装

wp package install dirtsimple/postmark

Postmark提供的两个主要命令是

  • wp postmark sync <file>... [--force] [--skip-create] [--porcelain]
  • wp postmark tree <dir>... [--force] [--skip-create] [--porcelain]

sync命令创建或更新与给定的.md文件匹配的文章或页面,而tree命令处理命名目录(及其所有子目录)中的所有.md文件。--porcelain选项使得输出静默,除了同步文件的WordPress文章/页面/选项ID。这意味着您可以通过将文件名传递给wp postmark sync --porcelain来获取文件的WordPress文章ID或选项ID。

默认情况下,除非自上次成功同步以来.md文件已更改(或已移动/重命名),否则不会更新文章和页面,但--force选项将覆盖这一点并同步所有命名的文件或目录,无论是否更改。这可能在您添加或删除影响文章转换或格式的插件时很有用。如果由于错误而部分同步文件,则在下一次运行类似命令时将再次尝试,即使未使用--force也是如此。

要使用WordPress同步Markdown文件,文件的元数据必须包括全局唯一标识符,在ID字段中。如果此值缺失,Postmark将自动添加它,除非您使用--skip-create选项(在这种情况下,您将收到错误消息)。

为了添加 ID,Postmark 必须能够写入相关文件和目录(在更改期间保存文件备份副本),因此如果 wp-cli 用户没有这些权限,您应该使用 --skip-create。 (此外,Postmark 假设您的元数据格式是这样的,即在元数据顶部添加 ID: 行不会产生语法错误,即您的顶层 YAML 不是用 {} 或其他任何东西包裹的。)

Postmark 提供的其他命令有

  • wp export [<spec>...] [--dir=<output-dir>] [--porcelain] [--allow-none] -- 将指定的帖子、页面或其他资源导出到 Markdown 文件中。 (有关更多信息,请参阅下面的 导出帖子、页面和其他资源。)
  • wp update [<file>...] [--porcelain] [--allow-none] -- 将数据库中的选定状态信息作为 .pmx.yml 文件导出到指定的 Markdown 文档旁边,以便可以通过 WordPress GUI 配置复杂属性(如页面构建器数据),但仍作为文本文件进行版本控制。 (有关更多信息,请参阅下面的 更新导出文档。)
  • wp postmark uuid [<file>...] -- 为每个尚未具有 ID: 字段的 文件 添加一个。 如果没有指定文件,它将输出一个适合用作新 .md 文件 ID: 的新 UUID。 (有关更多信息,请参阅下面的 ID: 字段。)

文件格式和目录布局

Postmark 期望看到具有 .md 扩展名和非空 Markdown 文件以及 YAML 元数据。如果文件名为 index.md,它将成为该目录或任何子目录中不包含自己的 index.md 的其他文件的 WordPress 页面/帖子父级。如果没有在 YAML 字段中覆盖,帖子或页面的默认 slug 是其文件名,去掉 .md 扩展名。如果文件名是 index.md,则使用包含目录的名称。

在同步任何单个 .md 文件时,Postmark 会向上搜索,直到找到一个匹配的 index.md 文件,或者找到一个“项目根”目录。 (项目根是包含 .git.hg.svn_postmark.postmark 子目录的任何目录。)找到的第一个此类 index.md 成为当前 .md 文件的父页面或帖子。 (如果它尚未存在于 WordPress 数据库中,它将递归向上搜索更多父级,直到每个父 index.md 都存在,并成为相应子帖子或页面的父级。)

Postmark 输入文件不需要放在您的 WordPress 目录下或由您的 web 服务器访问。出于安全考虑,它们不应由您的 web 服务器 写入,甚至不需要由运行 wp postmark 命令的用户 读取。您也不必将所有 Markdown 文件放在单个树中:postmark syncpostmark tree 命令分别接受多个文件和目录名称。

Postmark 输入文件使用标准的 YAML(v1.2)元数据,前后用 --- 分隔,如下所示

---
ID: urn:uuid:1e30ea5f-17fe-422a-9c24-cb591eb2d72d
Draft: true
---
## Content Goes Here

If no `Title` was given, the above heading is stripped from the post body and used instead.
But if a `Title` *was* given, the heading remains in place.

内容使用 league/commonmark 从 Markdown 转换为 HTML,格式化过程可以使用 Markdown 格式化操作和过滤器 进行扩展。

《ID:》字段

所有封页字段都是可选的,除了 ID:,它必须包含一个全局唯一标识符,最好是 UUID 格式。(您可以使用 wp postmark uuid 生成合适的值。)默认情况下,Postmark 会自动将字段添加到新的 Markdown 文件中,除非您使用 --skip-create 或 Postmark 无法写入文件或目录。

此标识符的目的是让 Postmark 能够匹配 WordPress 数据库中现有的页面或帖子,或者知道需要使用该标识符创建一个新的。 (帖子 ID 数字不足以完成此目的,因为它们在不同的 WordPress 安装中可能会有所不同,并且 WordPress 内部生成的基于 URL 的“GUID”在迁移到安装时通常会被更改。)

前文字段

除了必需的 ID: 字段外,您还可以包含以下任何或所有可选字段来设置 WordPress 中相应的数据。任何未包含的字段,或具有空或缺失值的字段,将不会从其当前值在 WordPress 中更改。

Title: # if missing, it's parsed from the first heading if the content starts with one
Slug:  # if missing, will be obtained from file/directory name

Category: something          # this can be a comma-delimited string, or a YAML list
Tags:     bar, baz           # this can be a comma-delimited string, or a YAML list
Author:   foo@example.com    # user id is looked up by email address or user login

Excerpt: |  # You can set a custom excerpt, which can contain markdown
  Some *amazing* blurb that makes you want to read this post!

Date: 2017-12-11 13:41 PST        # Dates can be anything recognized by PHP, and
Updated: April 30, 2018 3:46pm    # use Wordpress's timezone if no zone is given

Status:    # string, Wordpress `post_status`
Draft:     # unquoted true/false/yes/no -- if true or yes, overrides Status to `draft`
Template:  # string, Worpdress `page_template`
WP-Type:   # string, Wordpress `post_type`, defaults to 'post'

WP-Terms:  # Worpdress `tax_input` -- a map from taxonomy names to terms:
  some-taxonomy: term1, term2   # terms can be a string 
  other-taxonomy:               # or a YAML list
    - term1
    - term2

Comments:   # string, 'open' or 'closed', sets Wordpress `comment_status`
Password:   # string, sets Wordpress `post_password`
Weight:     # integer, sets Wordpress `menu_order`
Pings:      # string, 'open' or 'closed', sets Wordpress `ping_status`
MIME-Type:  # string, sets Wordpress `post_mime_type`

Post-Meta:  # array of meta keys -> meta values; only the given values are changed
  a_custom_field: "Good stuff!"
  _some_hidden_field: 42
  delete_me: null  # setting to null deletes the meta key

Set-Options:  # an option path or array of option paths; each will be set to the post's db ID
  - edd_settings/purchase_history_page  # e.g., make this page the "my account" page for both EDD
  - lifterlms_myaccount_page_id         # and LifterLMS.  See "Working With Options" for more info

HTML:  # override markdown conversion for specific fields
  Excerpt: "<p>This is html</p>"   # a string means, "use this HTML instead of what's in the field"
  body: true                       # non-false non-string means, "field is HTML, not markdown"

请注意,Postmark 仅验证或转换这些字段中的一小部分。大多数字段直接传递给 WordPress,如果使用无效值可能会导致问题。(例如,如果分配了一个实际上未安装的自定义帖子类型,或者帖子类型不支持的状态。)然而,在大多数情况下,只需将值更改为有效值并重新同步文件即可修复此类问题。

WordPress 插件或 wp-cli 软件包可以通过注册操作和过滤器来添加额外字段或更改现有字段的处理。

使用选项

许多 WordPress 插件具有特殊页面(如购物车、结账、“我的账户”等),它们在设置中作为帖子 ID 引用。WordPress 本身有一个设置主页(即 page_on_front)。有时还有一些需要 HTML 但希望能够以 Markdown 表达的选项,例如在版本控制文件中。

Postmark 提供了三种方法来集成此类 WordPress 选项。

  • 您可以通过将选项路径放入文档的 Set-Options: 封页字段中,将一个或多个 WordPress 选项(或其部分)设置为数据库文档 ID,从而使文档成为 WordPress 主页。(例如,Set-Options: page_on_front。)
  • 您可以使用文档的 ID: 作为 urn:x-option-id: URL 来就地更新可能已经存在的插件提供的帖子或页面。(选项在文档同步时设置,如果没有现有帖子/页面,则会创建一个。)
  • 您可以通过使用文档的 ID: 作为 urn:x-option-value: URL 来设置选项(或其部分)为文档生成的 HTML。

(此外,前两种集成方法实际上可以 结合 使用:您可以使用 urn:x-option-id: URL 作为文档 ID: 来就地更新插件提供的默认页面,然后使用文档的 Set-Options: 字段将其他选项指向同一页面。)

无论在某个文档中采用哪种方法,要处理选项,您都将使用 选项路径。选项路径是以 / 分隔的路径,从 WordPress 选项名称开始。第一个路径段之后的任何路径段被视为数组键,以遍历选项中的子项,并且如果路径段包含除字母数字、-_ 之外的内容,则所有路径段都必须进行 URL 编码。(例如,路径 foo/bar%2fbaz 指的是 foo 选项的 bar/baz 键。)

对于 Set-Options: 字段,您只需要放置一个选项路径(或它们的数组)来更新相关选项或其部分。对于 ID: 字段,您需要在路径前加上 urn:x-option-id:urn:x-option-value: 前缀,以区分选项中存储的是帖子 ID 还是 HTML。

选项引用

当插件创建一个存储帖子 ID 的选项的默认页面时,您可以通过将您的 Markdown 文档的 ID: 设置为 urn:x-option-id: URL 来就地更新该页面,例如:

ID: "urn:x-option-id:edd_settings/purchase_page"  # use this page as the EDD checkout page

具有 urn:x-option-id: URL 的 ID 的帖子将与正常帖子略有不同。与往常一样,如果存在具有给定 GUID 的帖子,Markdown 文件将同步到该帖子。之后,指定的选项将被编辑,以反映该帖子的 WordPress post_id。 (在上面的示例中,将创建或更改 edd_settings 选项下的 purchase_page 键)。

然而,如果不存在具有给定 GUID 的帖子,选项值(例如,在 edd_settings 选项下的 purchase_page 键)将检查有效的帖子 ID。如果存在并且引用了现有帖子,现有帖子的 GUID 将被更改,并通过同步覆盖其内容。 (这允许您在无需手动干预或创建重复帖子的情况下替换插件首次激活时生成的默认页面内容。)

其他可能有用的选项引用示例

AffiliateWP

  • urn:x-option-id:affwp_settings/affiliates_page

Easy Digital Downloads

  • urn:x-option-id:edd_settings/failure_page
  • urn:x-option-id:edd_settings/purchase_page
  • urn:x-option-id:edd_settings/purchase_history_page
  • urn:x-option-id:edd_settings/success_page

LifterLMS

  • urn:x-option-id:lifterlms_checkout_page_id
  • urn:x-option-id:lifterlms_memberships_page_id
  • urn:x-option-id:lifterlms_myaccount_page_id
  • urn:x-option-id:lifterlms_shop_page_id
  • urn:x-option-id:lifterlms_terms_page_id

WooCommerce

  • urn:x-option-id:woocommerce_cart_page_id
  • urn:x-option-id:woocommerce_checkout_page_id

(注意:这个列表可能远非详尽无遗,即使对于所列插件也是如此。此外,由于上述插件的最新版本可能会添加、重命名或删除任何这些设置,因此在生产环境中更新插件之前,您应始终在非生产数据库中测试您的同步。)

自定义CSS

WordPress 将主题的自定义 CSS 存储在类型为 custom_css 的帖子中,并在选项中保存帖子 ID。因此,您可以使用 urn:x-option-id:theme_mods_THEME/custom_css_post_idID: 从 Markdown 文件同步主题的自定义 CSS,其中 THEME 是主题的别名。

与其他 选项引用 一样,如果存在具有现有帖子 post id 的现有选项值,则该帖子将使用您的 Markdown 文件的 内容进行更新。或者,如果该值未设置(或默认为 -1),则将创建一个新帖子,并将选项值更新为指向该新帖子。

因此,要为 Hestia 主题定义自定义 CSS,您将创建一个类似这样的 Markdown 文件

---
ID:       urn:x-option-id:theme_mods_hestia/custom_css_post_id
WP-Type:  custom_css
Title:    hestia
Slug:     hestia
Comments: closed
Pings:    closed
Status:   publish
---

```css

/* CSS Content Goes Here */

```

代码围栏 css 是可选的:如果发现它位于类型为 custom_css 的帖子中,则将自动删除。 (这样做是为了您可以利用 Markdown 编辑器的任何 CSS 特定编辑或高亮显示功能。)您可以使用反引号或波浪号(~)进行围栏,只要至少有三个,开头和结尾围栏的长度相同且不缩进,开头围栏行上的第一个单词是 css 小写即可。

选项HTML值

一些 WordPress 插件具有包含 HTML 内容的选项,您可能更愿意使用 Markdown 编写并维护在修订控制之下。您可以使用每个文档的 ID: 中的 urn:x-option-value: URL 将文件同步到这些设置,例如:

ID: "urn:x-option-value:edd_settings/purchase_receipt"

具有上述 ID: 的帖子将通过将文档主体转换为HTML进行同步,并将结果保存到 edd_settings 选项的 purchase_receipt 键中。大多数其他前置内容都会被忽略(除了用于 原型和模板 的内容)并且 不会实际创建任何帖子,因此在整个过程中只会调用 Markdown格式化选项同步 钩子,并且命令输出将列出 ID: 而不是Wordpress的数字帖子ID。

由于选项没有元字段,选项的同步时间戳保存在一个(非自动加载)选项中,即 postmark_option_cache,从而避免了更改未变文档的不必要的更新。(然而,删除此选项是安全的,因为唯一的影响将是有效地将任何ID为选项值的文档的下次同步强制为 --force。)

原型和模板

在某些情况下,您可能有很多具有常见字段值或结构的文档。您可以通过创建 原型 来保持项目DRY(即,不要重复自己)。例如,如果您有很多包含一个或多个视频和一些介绍性文本的“视频”页面,您可以创建以下文件

---
Prototype: video
Videos:
 - title: First Video
   url: https://youtube.com?view=example
 - title: Second Video
   url: https://vimeo.com/something
---
# Example Videos

Dude, check these out!

然后,在同一目录或父目录中创建一个包含 video.type.yml 文件的 _postmark/.postmark/ 目录,该文件包含常见的属性

WP-Type: post
Draft: false
Category: videos
Author: me@example.com

以及一个包含正文中文本的 video.type.twig 文件,这是一个 Twig模板

{{ body }}

{% for video in Videos %}
## {{ video.title }}
[video src="{{ video.url }}"]
{% endfor %}

然后,任何在其前置内容中包含 Prototype: video 的文档都将具有指定的帖子类型、类别和作者,并且通过在正文后添加 Videos: 列表中的任何项目进行格式化。

或者,如果您更愿意通过单个文件指定类型,您可以将属性和模板合并到一个单独的 video.type.md 文件中,将属性放在前置内容中,并将(如果有的话)Twig模板放在正文中。(与 custom_css 帖子类似,正文可以选择性地包裹在一个带有 twig 语言的代码块中,如果您想利用Markdown编辑器中特定的编辑或高亮支持。)

如果存在与 .type.yml 和/或 .type.twig 一起的 .type.md 文件,则 .type.yml 中的属性将覆盖 .type.md 中的属性,并且 .type.twig 中的模板将 包裹 .type.md 中模板的输出。

模板处理

Twig模板(在 .type.twig.type.md 中)用于生成 Markdown(不是HTML),可能还包含Wordpress短代码。模板在 同步时间 被静态处理,而不是在Wordpress页面生成期间,并且只能访问正在同步的文档中的数据。提供给模板的“变量”是前置内容的属性,以及 body 用于正文文本。

模板可以使用完整的Twig语法,包括宏、extends 标签和 include() 函数,这意味着您可以将其他模板文件放在您的 _postmark.postmark 目录中,然后将其用作部分或基本模板,类似于其他静态站点生成器。例如,我们可以在上面的示例中做如下操作

{% from "macros.twig" import video_block %}
{{ body }}

{% for video in Videos %}
{{ video_block(video) }}
{% endfor %}

其中包含在我们的 _postmark.postmark 目录中的 macros.twig

{% macro video_block(video) %}
## {{ video.title }}
[video src="{{ video.url }}"]
{% endmacro %}

继承和模板重用

支持有限的原型继承形式:如果一个原型有包含 Prototype:.type.md 文件,那么该原型的属性将被视为 .type.md 的默认值。(递归地,如果第二个原型本身也有 Prototype:)。只继承属性,不继承模板,因为将模板应用于模板通常没有用。如果您需要在多个原型之间共享模板,请将其放入单独的 .twig 文件中,然后使用 Twig 的 include()(或 extendsimport)在每个需要的位置应用它。

变更检测

为了使同步尽可能快,Postmark 将导入文档的信息缓存到 WordPress 数据库中,并且仅在文档(或其原型文件)实际更改时更新数据库。

Postmark 缓存的信息包括文档的前置内容和主体内容的哈希值,在继承原型和应用模板之后。这确保了如果文档或其原型文件有任何更改,则数据库将更新为新结果。

然而,此过程不会自动检测对插件、动作、过滤器等所做的更改,这些更改会影响文档如何渲染为 HTML 或哪些数据将插入到数据库中。除非您使用 --force 来同步所有文档,或者您在您的前置内容中添加额外的字段,否则这些更改通常不会被捕获。

例如,您可以在您的原型中添加一个 Prototype-Version 字段,然后更改此字段的值以触发使用该原型的所有文档的更改。

当然,如果您正在创建 Postmark 扩展(例如在 wp-cli 包、插件、主题或 Imposer 状态模块中),这不会帮到您。假设您知道要编辑什么,您也不能编辑您用户的原型文件。

但是您可以在导入时“编辑”您用户的 前置内容,使用 postmark load wp-post 动作。例如

add_action('postmark load wp-post', function($doc) {
    if ( $doc->has('EDD') ) $doc['EDD-Importer-Version'] = '4.1';
}, 10, 1);

add_action('postmark_metadata', function($postinfo, $doc) {
	if ( ! $doc->get('EDD') ) return;
    // ...  code to import various things to $postinfo
}, 10, 2);

在这个例子中,postmark load wp-post 处理器在加载包含 EDD 字段的文档时添加了一个额外的 EDD-Importer-Version 字段。这意味着如果 EDD 字段的导入语义发生更改,可以更改版本,然后任何包含 EDD 字段的文档都将被视为“已更改”,因为它们上次同步以来已经更改。这样,仅仅升级插件(或包、状态模块、主题等)就会自动使受影响文档的缓存无效。

(顺便说一下,这种类型的版本化 对于会改变 HTML 格式或需要访问 postinfo 对象的扩展是必需的。如果扩展只是提供语法糖或字段映射,并且可以完全从 postmark load wp-post 动作中完成它所需的一切,那么映射的字段已经包含在文档哈希中,因此映射过程中的任何更改都会自动更改受更改影响的任何文档的哈希值。)

Imposer集成

Postmark 为与 imposer 的可选集成提供了一个 状态模块:只需将类似这样的壳块添加到您的 imposer-project.md 或任何需要将 markdown 内容作为其状态一部分的状态模块中即可。

require "dirtsimple/postmark"      # load the API

# Use postmark-module for prepackaged .md files that should be read-only and have
# ID: values already:

postmark-module "$__DIR__/stuff"   # sync `stuff/` next to this file, with --skip-create option

# Or use postmark-content for writable directories where you might be adding new .md
# files without an existing ID:

postmark-content "my-content"      # sync `my-content/` at the project root

您实际上可以使用此方法分发包含 markdown 内容的 wp-cli 包,并且它们将自动加载到使用它们的网站(们)上。

注意:当调用 postmark-modulepostmark-content 函数时,它们不会立即同步。相反,它们将目录信息记录在 imposer JSON 规范对象中,以便在 imposer apply 的任务运行阶段进行后续解析。(有关更多信息,请参阅 imposer 文档。)

导出文章、页面和其他资源

为了便于处理特殊帖子类型和其他数据库资源,postmark 提供了 wp postmark export 命令,该命令根据帖子 ID、GUID、URL 或 imposer 引用创建指定目录中的 markdown 文件。(例如 @my-appt-type:id:285)。只要已注册了导出函数,任何 资源类型 都可以导出。

每个导出文件都根据其别名(即其 post_name)命名,可能以一个 - 和一个数字结尾。如果已存在具有相同名称的文件,则会检查其 GUID —— 如果相同,则覆盖文件;否则,递增数字并检查下一个候选文件。

(例如,如果有十个具有唯一 GUID 的帖子以 post_namefoo 的名称导出,它们将分别存放在 foo.mdfoo-1.md、一直到 foo-9.md。如果这十个帖子中的任何一个被重复导出,如果没有删除且 post_name 未更改,则将使用之前使用的相同文件名。)

目前,帖子内容以原始格式导出到 markdown 文件中,没有尝试将 HTML 转换回 markdown。此外,许多默认值或空字段可能包含在 YAML 前置信息中。因此,导出的帖子必须手动编辑以解决这些问题。或者,您可以使用 导出操作和过滤器 在导出过程中转换或清理内容。(例如,删除不应置于版本控制下的元字段。)

由于帖子摘要和正文以 HTML 存储在数据库中,导出的文档将相关的 HTML: 前置信息字段设置为 true,因此如果文件直接导入,内容不会被重新解析为 markdown。如果您将内容转换为 markdown,则应从 HTML: 映射中删除相关条目,或者如果您将在 WordPress 中编辑内容并使用 postmark update 保存它,则将其重命名为 Export-HTML:

另外注意:帖子父级、菜单顺序和 MIME 类型目前 包含在其导出文件中,因为菜单顺序和 MIME 类型仅用于菜单项和附件,而 postmark 通过目录位置确定帖子的父级(如果有)。(帖子的 _thumbnail_id 元数据也被排除,因为它引用了可能在不同数据库中变化的整数 ID。)

更新导出文档

许多 WordPress 插件和主题为帖子提供额外的设置或数据,这些设置或数据难以通过前置信息手动指定。例如,页面构建器、访问限制工具、页面特定主题选项等。

为了在支持版本控制和开发/生产部署的同时协助处理这些功能,Postmark 提供了 postmark update 命令。该命令将更新的帖子属性导出到与原始 markdown 文档相邻的 .pmx.yml 文件中,这些属性在同步期间自动合并到帖子中。使用单独的文件可以避免在主文档的前置信息中丢失注释、间距等,并且前置信息中指定的任何字段都会覆盖 .pmx.yml 文件中的字段。

为了导出帖子元数据字段,必须在文档的 Export-Meta: 前置内容中列出(或者从原型继承)。例如,在文档的前置内容中加入以下内容,就可以使用 Elementor 进行的更改通过 postmark update 保存,然后通过 postmark syncpostmark tree 应用于相同或另一个数据库。

Export-Meta:
  # On `postmark update`, export primary Elementor fields
  _elementor_edit_mode:
  _elementor_template_type:
  _elementor_version:
  _elementor_data:

Post-Meta:
  # Forcibly delete the CSS cache on import, so it won't be stale
  _elementor_css: null

Export-HTML:  # Use HTML from Elementor instead of the markdown body
  body:

注意,为了安全地使用此功能,您需要了解您的插件提供的各种元数据字段 做什么,以免在部署时损坏数据。

例如,Elementor 包含一个 _elementor_css 字段,导入时应该始终 删除 以确保正确的 CSS,而 LifterLMS 有一个 _llms_num_reviews 字段,不应导入以避免数据丢失。某些插件可能会根据其他字段生成值,通常仅在通过 UI 更新帖子时设置,而不是通过命令行。因此在提交和部署您的 .pmx.yml 文件之前要小心。

设置 Export-Meta 字段的一个简单方法是从现有帖子或页面导出,然后编辑导出的文件,将 Post-Meta: 字段重命名为 Export-Meta:。然后,删除任何不应通过导入重置的字段,以及您希望保留的字段的值。一旦您对所需的字段有了良好的了解,您可能希望将它们添加到原型中,这样您就不必将它们复制到多个文件中。单个文档可以添加任何额外字段,或通过设置值为 false 来抑制字段的导出。例如,以下操作将阻止当前文档导出 _some_field,即使其原型列出了导出。

Export-Meta:
  _some_field: false

对于 Export-Meta 中的每个非 false 条目,在 .pmx.yml 导出文件的 Post-Meta 中都会有相应的条目:该元数据字段的值,或者如果帖子缺少该字段则为 null。这意味着在导入时,将显式删除这些缺失字段,从而删除数据库中的任何悬空值。这对于使用基于元数据字段的缺失或存在来决定事物的插件尤其重要,而不仅仅是它的内容。

更新导出HTML

如果您将使用 WordPress 图形界面(例如使用 Gutenberg 或页面构建器)编辑或生成帖子内容或摘要,您应该在文档中添加一个 Export-HTML: 字段,列出要导出的字段,并从文档(以及如果存在则从 HTML: 字段)中删除相应的字段。所以,如果您想要创建一个主要内容将通过 Gutenberg 编辑器进行编辑的新文档,您可能会创建一个如下所示的新空文档

---
Title: An Example
WP-Type: page

Export-HTML:
  body:
  Excerpt:
---

将此文档导入到 WordPress 页面后,您可以使用 postmark update 导出正文和摘要的 HTML。

(相反,如果您已在 WordPress 中创建了该文档,您可以使用 postmark export 创建初始的 markdown 文件,但您需要编辑它并将 HTML: 字段重命名为 Export-HTML:,删除正文文本和 摘要:,然后在该文件上运行 postmark update 以替换相应的 .pmx.yml 文件中的旧正文和摘要。)

更新其他字段

除了元数据和 HTML 字段外,您还可以允许在执行 postmark update 时从 WordPress 图形界面更新其他字段:只需将字段添加到 Export-Fields:。例如,此前置内容将导致在更新期间将 Updated:Tags: 字段导出到 .pmx.yml 文件。

---
Export-Fields:
  Updated:
  Tags:
---

记住,虽然由postmark update导出的字段只是默认值:你必须从主.md文件中删除相应的字段,才能导入导出的数据。

操作和过滤器

Postmark的所有操作和过滤器都可以从插件、主题、wp-cli包或imposer状态模块注册。由于Postmark是建立在imposer之上的,你可以将imposer_tasks动作钩子用于注册其他动作和过滤器——这意味着你可以将你的imposer或postmark特定的钩子放在一个单独的PHP文件中,然后通过require_once包含该文件,例如:

add_action('imposer_tasks', function() {
    require_once(__DIR__ . '/includes/cli-hooks.php');
});

然后,你可以在includes/cli-hooks.php中放置任何注册postmark动作或过滤器的代码,并且该文件仅在运行wp postmarkimposer时加载。

Markdown格式化

Markdown格式化由以下过滤器控制

  • apply_filters('postmark_formatter_config', array $cfg, Environment $env) -- 这个过滤器在每个命令中只调用一次,用于初始化League/Commonmark 环境配置。过滤器可以向Environment对象添加Markdown扩展、解析器、处理器或渲染器,或者返回一个修改后的$cfg数组。除了标准配置元素之外,$cfg还包含一个将扩展或解析器类名映射到参数数组(或null)的extensions数组。使用给定的参数数组实例化这些扩展类,并将它们添加到$env中。可以通过将extensions数组中的值设置为false来禁用扩展。

    当前默认扩展包括

  • apply_filters('postmark_markdown', string $markdown, Document $doc, $fieldName) -- 这个过滤器可以在将文档(或其任何前导字段)转换为HTML之前修改文档的Markdown内容。$fieldName"body",如果$markdown来自$doc->body;否则,它是正在转换的前导字段名称。(如"摘要",或任何由插件添加的自定义字段。)

  • apply_filters('postmark_html', string $html, Document $doc, $fieldName) -- 这个过滤器可以在将文档(或其任何前导字段)转换为HTML之后立即修改其HTML内容。与postmark_markdown过滤器类似,$fieldName"body"或前导字段名称。

请注意,postmark_markdownpostmark_html可能会被多次调用或根本不会被调用,因为它们是在调用$doc->html(...)时运行的。如果同步过滤器或动作在Postmark有机会之前设置了$postinfo['post_content']$postinfo['post_excerpt'],则除非过滤器或动作使用$doc->html(...)进行转换,否则这些过滤器不会被调用。

此外,请注意,如果您正在向这些过滤器中的任何一个添加钩子,您还应该在相关的 postmark load 过滤器期间将格式化版本信息添加到文档中,这样当您的扩展添加、删除或更新时,受影响的任何文档都将被视为“已更改”,并重新同步。

文档对象

许多过滤器和操作接收 dirtsimple\Postmark\Document 对象作为参数。这些对象提供了以下 API

  • 前端字段可以作为公共、可写对象属性访问。(例如,$doc->Foo 返回前端字段 Foo)。无效的 PHP 属性名字段可以通过例如 $doc->{'Some-Field'} 访问。缺失或空字段返回 null;如果字段缺失时您希望有不同的默认值,可以使用 $doc->get('Somename', 'default-value')
  • $doc->body 是文档的 Markdown 文本,是一个可写属性。
  • $doc->html($propName='body') 将指定属性从 Markdown 转换为 HTML(触发 postmark_markdownpostmark_html 过滤器)。
  • $doc->has("field") 如果文档具有“field”作为其前端字段之一,则返回 true
  • $doc->get("field", $default=null) 返回前端中“field”的内容,或如果没有找到则返回 $default
  • $doc->select(['field' => callback, ...]) 如果存在匹配的字段,则调用每个 callback,并使用该字段的值。返回值是一个仅包含字段键的数组,其值是调用回调的结果。如果回调实际上不可调用(例如 true),则值以原样返回到输出数组中。如果回调是一个关联数组,则递归处理,因此例如 $doc->select(['EDD' => ['Price' => $cb]]) 将在存在具有 Price 子字段的 EDD 前端字段的情况下调用 $cb

postmark load resource-kind

每次从磁盘加载文档时,都会运行 do_action("postmark load $kind", Document $doc),以便在计算文档哈希之前修改文档(例如其 Post-Meta)。在此操作期间对文档所做的任何更改都将影响哈希计算,因此这是进行简单的语法糖或字段映射的理想位置。

但是,如果您正在编写需要执行复杂计算或访问数据库的扩展,则可能应该使用不同的钩子,并在此操作期间添加版本字段(例如 MyPlugin-Version-Info),以确保在您的算法更改时文档重新同步。同样,如果您的扩展正在更改 Markdown 格式化方式,则应添加版本字段,以确保添加、删除或更新您的扩展将强制受影响的文档重新同步。

默认的 resource-kindwp-post,这意味着文档将被映射到 WordPress 文章,或者如果文档具有 x-option-value URL,则默认资源类型是 wp-option-html。可以使用 postmark_resource_kinds 动作 注册其他资源类型,并通过 Resource-Kind: 前端字段(直接在文档中或通过原型)分配给文档。

postmark_resource_kinds

Postmark 不仅用于导入文章和选项值。原则上,它可以用于导入其他内置 WordPress 对象(如用户或类别)或由插件定义的专用对象(如存储在自定义数据库表中的 Gravity Forms)。

为了确定要导入的对象类型,Postmark 会查看文档的 Resource-Kind 字段,默认情况下为 wp-post(除非使用了 x-option-value: URL 的 ID:,在这种情况下,默认类型为 wp-option-html)。但您可以通过文档的前端或其原型覆盖这些默认值,只要插件已注册该资源类型的导入处理程序。

为了添加其他资源类型,扩展可以为postmark_resource_kinds动作注册钩子,该钩子将接收一个类似数组的对象,该对象将类型名称映射到“类型定义”对象。注册此动作的处理程序可以使用诸如setImporter()setExporter()setEtagQuery()之类的配置方法来配置类型。例如,以下代码为my_plugin-item资源类型注册了导入器和导出器

add_action('postmark_resource_types', function($kinds) {
    $kinds['my_plugin-item']->setImporter('my_plugin_import_item_from_doc');
    $kinds['my_plugin-item']->setExporter('my_plugin_export_item_to_doc');
});

function my_plugin_import_item_from_doc($doc) {
    # import $doc into database, saving $doc->etag() with it for caching purposes,
    # then return a database ID or WP_Error
}

function my_plugin_export_item_to_doc($md, $id, $dir, $doc=null) {
    # Export an database object whose ID is $id by setting values on the
    # MarkdownFile in $md, returning a slug that will be used to generate
    # the export filename.  Return `false` if $id isn't found, or a WP_Error
    # to signal other error conditions.  If $doc is non-null, the export is
    # an update to an existing document, and $doc can be used to trim or
    # filter the output fields accordingly (e.g. the way posts use `Export-Meta:`).
}

每当文档具有Resource-Kind:my_plugin-item时,将调用my_plugin_import_from_doc()函数来执行导入,替代Postmark内置的同步处理。

为了避免对未更改的文件进行不必要的数据库更新,Postmark为文档内容计算了一个etag(可以通过$doc->etag()方法获取)。导入器应在导入时将此值保存到数据库中,并在同步开始时注册一个处理程序来检索那些etag。

例如,如果我们想为WordPress用户创建导入器,但不想在导入文件(或文件)未更改时更新用户数据,我们需要注册导入器和etag查询,例如

function demo_user_importer($doc) {
    # Create or update a user
    if ( $user_id = email_exists($doc['Email']) ) {
        $user_id = wp_update_user(...);  # ...with appropriate data
    } else {
        $user_id = wp_insert_user(...);  # ...with appropriate data
    }

    # Return error if insert or update failed
    if ( is_wp_error($user_id) ) return $user_id;

    # save $doc->etag() for caching
    update_user_meta($user_id, '_postmark_cache', wp_slash($doc->etag()));
    return $user_id;
}

add_action('postmark_resource_types', function($kinds) {
    global $wpdb;
    $kinds['wp-user']->setImporter('demo_user_importer');
    $kinds['wp-user']->setEtagQuery(
        "SELECT user_id, meta_value FROM $wpdb->usermeta WHERE meta_key='_postmark_cache'"
    );
});

在上面的示例中,使用一个_postmark_cache元字段来存储etag,并通过简单的SQL查询来检索它。所使用的查询必须以正确的顺序返回数据库ID和etag,作为每个项目的适当类型的前两个字段。

当然,在某些情况下,无法直接查询数据库以获取必要的信息。例如,Gravity Forms不使用WordPress风格的元数据为其表单,所以包含的Gravity Forms示例扩展使用setEtagOption('gform_postmark_etag')而不是setEtagQuery()来配置资源类型的etag处理。这告诉Postmark自动从指定的选项保存和加载etag。

这种方法的缺点是,您需要编写代码在相关数据库项被删除时从选项中删除已删除的ID。(在上面的用户示例中不需要这样做,因为删除用户会自动删除相关的元字段。)

最后,如果数据库查询或选项对您的资源类型都不适用,您可以使用setEtagCallback($callback)来注册一个不带参数将被调用的函数,该函数应返回一个从数据库ID映射到其相关etag的数组。(与setEtagQuery()一样,您的导入函数将负责将etag存储在数据库中。)

文章的同步动作

在帖子同步过程中,文档将构建一个$postinfo数组以传递给wp_insert_postwp_update_post。 (Postmark只设置已由动作或过滤器设置的值中的值,因此您可以通过首先设置值来防止它这样做。)

例如,Postmark在调用postmark_metadata动作之后、调用postmark_content动作之前计算post_content。这意味着您可以从postmark_before_sync动作或postmark_metadata动作中设置post_content来防止Postmark进行Markdown到HTML的转换。

注意:$postinfo实际上不是一个PHP数组--它是一个PHPArrayObject子类,具有一些额外的方法,如get($key, $default=null)has($key)等。但是您仍然可以将其视为常规数组来设置、获取或删除项。您可以通过查看dirtsimple\imposer\Bag类了解大多数其他可用方法,但还有一些可能对您有帮助的额外方法。

  • $postinfo->id() 返回正在更新的现有帖子的帖子ID,如果帖子是新帖子,则返回null。
  • $postinfo->set_meta($key, $val) -- 执行 update_post_meta,将 $key 设置为 $val$key 可以是一个字符串或一个数组:如果是数组,它被视为元字段中子项的路径,类似于 wp-cli wp post meta patch insert 命令的键路径,但会自动创建父数组。
  • $postinfo->delete_meta($key) -- 删除指定的元键,如果 $key 是一个数组,它被视为元字段中子项的路径,类似于 wp-cli wp post meta patch delete 命令的键路径。

以下操作在同步过程中运行(对于帖子,不是选项),按照以下顺序

postmark_before_sync

do_action('postmark_before_sync', Document $doc, PostModel $postinfo) 允许在同步之前修改文档(例如 Post-Meta 字段)或执行其他操作。此动作可以设置 $postinfo 对象中的 WordPress 帖子字段(例如 post_authorpost_type),以防止 Postmark 对这些字段执行默认的翻译。(然而,在这个时候,对象大多为空,因此从中读取并不很有用。)将 $postinfo->wp_error 设置为 WP_Error 实例将强制同步以给定的错误终止。

postmark_metadata

do_action('postmark_metadata', PostModel $postinfo, Document $doc) 允许您修改将传递给 wp_insert_postwp_update_post$postinfo。此钩子可用于覆盖或扩展基于前端内容的 WordPress 字段的计算。

当此动作运行时,$postinfo 将初始化为 Postmark 从前端内容计算出的任何 WordPress 字段值,或者由 postmark_before_sync 动作在 $postinfo 中设置的值。然而,它不包含 post_contentpost_excerpt,除非之前的操作或过滤器设置。

为此动作注册的函数可以设置 $postinfo 中的 post_contentpost_excerpt 以阻止 Postmark 执行操作。它们还可以设置 $postinfo['wp_error'] 为 WP_Error 对象以错误终止同步过程。

如果处理此动作注册的所有函数后,post_contentpost_titlepost_excerptpost_status 仍然为空,Postmark 将通过将文档主体从 Markdown 转换为 HTML,以及/或提取所需的标题和摘要来提供默认值。

postmark_content

do_action('postmark_content', $postinfo, Document $doc)postmark_metadata 动作类似,但如果有必要,已进行 markdown 转换和标题/摘要提取。

postmark_after_sync

do_action('postmark_after_sync', Document $doc, WP_Post $rawPost) 允许在文档和/或生成的帖子上进行帖子同步操作。 $rawPost 是一个 raw 过滤的 WordPress WP_Post 对象,反映了现在已同步的帖子。这可以用于处理需要知道帖子 ID 的前端字段(例如,向自定义表添加数据)。

选项的同步动作

postmark_before_sync_option

do_action('postmark_before_sync_option', Document $doc, array $optpath) 在处理同步到 选项 HTML 值 的文档之前运行。没有 $postinfo,因为没有要创建或更新的帖子。然而,此过滤器仍然可以访问或修改文档的任何其他属性,例如在更新选项之前以某种方式预处理主体。为了方便,$optpath 包含正在同步的选项的路径,例如 ['edd_settings', 'purchase_receipt']

postmark_after_sync_option

do_action('postmark_after_sync_option', Document $doc, array $optpath)postmark_before_sync_option 类似,区别在于它是在选项值从文档体的 HTML 版本更新后运行的。此钩子的一个典型用途是从文档的前置内容更新其他选项,例如:

use dirtsimple\Postmark\Option;

add_action('postmark_after_sync_option', function($doc, $optpath){
    if ( $optpath === ['edd_settings', 'purchase_receipt'] ) {
        if ( $doc->has('Title') )  Option::patch(['edd_settings', 'purchase_subject'], $doc->Title);
        if ( $doc->has('Header') ) Option::patch(['edd_settings', 'purchase_heading'], $doc->Header);
    }
}, 10, 2);

上面的示例代码将在同步任何 ID 为 urn:x-option-value:edd_settings/purchase_receipt 的文档后运行,检查文档的标题和/或标题字段,然后使用它们设置相关的 EDD 选项。

导出动作和过滤器

在以下每个动作和过滤器中,$md 参数是一个 dirtsimple\Postmark\MarkdownFile 对象,其 body 是正在导出的帖子的 post_content,其他属性将作为 YAML 前置内容导出到文档的头部。这两个动作和过滤器都可以根据需要读取、设置或取消设置这些属性,从而改变将要写入输出文件的内容。

以下钩子按执行顺序列出

postmark_export_meta

do_action('postmark_export_meta', $postmeta, MarkdownFile $md, WP_Post $post) 允许您修改 $postmeta 的内容(这最终将填充导出文档的 Post-Meta: 字段,如果其中有内容的话)。您可以使用此功能取消设置在输出中无用的元值,或从它们设置文档字段。(例如,postmark 自己从 $postmeta['_wp_page_template'] 设置 $md->Template 并从 $postmeta 中取消设置。)

$postmeta 对象是一个 Bag(ArrayObject 子类,具有 额外方法),支持正常的数组操作,如 $postmeta['foo']="bar",以及如 has()get()select() 这样的方法。

postmark_export_meta_$key

do_action("postmark_export_meta_$key", $meta_val, MarkdownFile $md, WP_Post $post) 在运行 postmark_export_meta 钩子后,对 $postmeta 中仍存在的每个元字段运行。 $meta_val 是字段的值。

如果有任何钩子注册了这个动作,相应的 $key 将从导出文档的 Post-Meta: 字段中删除。(这意味着您可以使用 add_action("postmark_export_meta_somekey", "__return_true"); 作为抑制元键导出的简单方法(例如,包含不应保存在导出文件中的动态状态的元键。)

postmark_export

do_action('postmark_export', MarkdownFile $md, WP_Post $post) 在写入输出文件之前,为每个导出调用一次,带有一个完全填充的 MarkdownFile 对象。您可以使用此功能根据需要添加、更改或删除字段。(例如,为存储在其他表中的数据的自定义帖子类型添加字段。)

postmark_export_slug

apply_filters('postmark_export_slug', string $slug, MarkdownFile $md, WP_Post $post, $dir) 过滤用于生成导出文件名的 slug。 $dir 是输出目录,可以是空字符串(对于当前目录)或带尾随 / 的目录名。初始值 $slug 来自 $md->Slug,在 postmark_export 钩子有机会更改它之后。

postmark update wp-post

do_action('postmark export wp-post', Bag $export, MarkdownFile $md, WP_Post $post, Document $doc) 在通过 postmark update 命令更新现有文档($doc)时由其对应的 $post 运行。变量 $md 包含如果执行正常导出将写入的 MarkdownFile 对象,而 $export 是将内容写入 .pmx.yml 文件的 ArrayObject。为此动作注册的回调可以修改 $export 以更改将要写入的数据。

虽然大多数对此操作的回调函数只需要前两个参数,但可以使用$doc参数来查找标志以决定如何导出。例如,你可以检查$doc->has('Export-Foo')以决定是否导出某些数据,类似于内置的Export-HTMLExport-Meta字段。在$doc中找到标志后,然后将从$md(或$post)中的相关数据复制到$export的字段中。

其他过滤器

postmark_author_email

apply_filters('postmark_author_email', string $author, Document $doc)过滤Author:前端字段(如果存在),以提取用于查找文章作者数据库ID的电子邮件或登录名。此过滤器接受一个字符串,并应返回一个电子邮件、登录名或WP_Error对象。如果字符串已经是有效的电子邮件或登录名,则过滤器应返回它不变。

只有当前端有Author:字段且postmark_before_sync尚未设置post_author时,才会调用此过滤器。在内部,postmark将此结果视为Imposer @wp-user引用键,因此,从技术上讲,你可以返回任何Imposer可以识别为@wp-user引用的内容,无需特定键类型。

postmark_excluded_types

apply_filters('postmark_excluded_types', array $post_types)过滤一个数组,其中包含postmark导入的类型。默认情况下,该数组包含['revision', 'edd_log', 'edd_payment', 'shop_order', 'shop_subscription'],以排除修订版和EDD/WooCommerce订单。如果你使用任何具有自定义文章类型的插件,这些类型可以增长到数千篇文章,但不需要导入,将文章类型添加到此列表中将有助于保持postmark的启动速度快,通过减少需要从数据库加载的GUID数量。

项目状态/路线图

该项目仍处于早期开发阶段:测试不存在,CLI输出的i18n不完整。我希望包含的未来的功能有

  • 标记摘要提取分割点的某种方法(最好与摘要中的链接指向目标页面上的断点进行链接定位)
  • 处理图片/附件的某种方法
  • 从相对文件链接到绝对URL的链接翻译

请参阅完整的路线图/待办事项列表