Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f0cc20bbf | ||
|
|
f14846d758 | ||
|
|
e2dcbc1fc3 | ||
|
|
731d6b51d1 | ||
|
|
abef5b70da | ||
|
|
fb58679b6f |
@ -770,6 +770,7 @@ message ProvisionerEnsureSettingResponse {}
|
||||
// PageSeed mirrors plugin.PageConfig.
|
||||
message PageSeed {
|
||||
string slug = 1;
|
||||
// Legacy slug-based parent reference. Prefer parent_key for new page seeds.
|
||||
string parent_slug = 2;
|
||||
string title = 3;
|
||||
string template_key = 4;
|
||||
@ -779,6 +780,16 @@ message PageSeed {
|
||||
string detail_slug_field = 8;
|
||||
bool reconcile_blocks = 9;
|
||||
bool reconcile_template = 10;
|
||||
// Stable identity scoped to the declaring plugin.
|
||||
string key = 11;
|
||||
// Stable identity of a parent page declared by the same plugin.
|
||||
string parent_key = 12;
|
||||
// Applied only when the page is created; later reconciliation does not move
|
||||
// an existing page when this value changes.
|
||||
string mount_path = 13;
|
||||
// Shallowly add absent top-level defaults to matching plugin-owned block
|
||||
// assignments while preserving all present values, including empty values.
|
||||
bool merge_missing_block_content = 14;
|
||||
}
|
||||
|
||||
message ProvisionerEnsurePageRequest {
|
||||
@ -802,6 +813,10 @@ message ProvisionerEnsureMenuItemRequest {
|
||||
string url = 3;
|
||||
string page_slug = 4;
|
||||
int32 sort_order = 5;
|
||||
// Stable plugin/page-key target. When set, these identify the provisioned
|
||||
// page independently of its current slug.
|
||||
string page_plugin = 6;
|
||||
string page_key = 7;
|
||||
}
|
||||
|
||||
message ProvisionerEnsureMenuItemResponse {}
|
||||
|
||||
@ -246,6 +246,9 @@ message BridgeCallRequest {
|
||||
string service_name = 1;
|
||||
string method = 2;
|
||||
bytes payload = 3;
|
||||
// Host-authenticated identity of the consumer plugin. The host derives this
|
||||
// from the loaded caller; providers must not trust identity in payload.
|
||||
string caller_plugin = 4;
|
||||
}
|
||||
|
||||
message BridgeCallResponse {
|
||||
|
||||
@ -44,6 +44,23 @@ message PoweredBlock {
|
||||
string template = 1;
|
||||
// JSON encoding of the template data map (map[string]any).
|
||||
bytes data_json = 2;
|
||||
// Requested HTTP response status. Before writing headers, the host accepts
|
||||
// only 200–599, uses the highest status class, and keeps the first result
|
||||
// encountered within that class.
|
||||
int32 status_code = 3;
|
||||
// Structured document metadata merged by the host into the page head. The
|
||||
// first non-empty value for each field wins in deterministic render order.
|
||||
DocumentMetadata metadata = 4;
|
||||
}
|
||||
|
||||
// DocumentMetadata is the safe, shared subset of document head data a plugin
|
||||
// may provide. The host validates and escapes every value before rendering.
|
||||
message DocumentMetadata {
|
||||
string title = 1;
|
||||
string description = 2;
|
||||
string canonical_url = 3;
|
||||
string image_url = 4;
|
||||
string robots = 5;
|
||||
}
|
||||
|
||||
// RenderTemplateRequest renders one template (templates.TemplateFunc).
|
||||
@ -76,6 +93,11 @@ message TemplateDocument {
|
||||
// Extra <html>-element attributes (e.g. data-theme). A "lang" key here is
|
||||
// ignored — the lang field wins.
|
||||
map<string, string> html_attrs = 6;
|
||||
// Requested HTTP response status. The host applies the same validation and
|
||||
// precedence rules as PoweredBlock.status_code.
|
||||
int32 status_code = 7;
|
||||
// Structured document metadata merged by the host into the page head.
|
||||
DocumentMetadata metadata = 8;
|
||||
}
|
||||
|
||||
// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG).
|
||||
@ -181,6 +203,10 @@ message PageContext {
|
||||
string title = 3;
|
||||
string post_type = 4; // "page", "post", "master", "system"
|
||||
string status = 5; // "published", "draft", "scheduled"
|
||||
// Slug derived from the resolved page hierarchy.
|
||||
string derived_slug = 6;
|
||||
// Effective mount path after host inheritance and normalization.
|
||||
string mount_path = 7;
|
||||
}
|
||||
|
||||
// PostContext mirrors blocks.PostContext.
|
||||
@ -276,4 +302,8 @@ message BlockContext {
|
||||
bytes detail_row_data_json = 36;
|
||||
string blog_index_url = 37;
|
||||
string category_page_url = 38;
|
||||
// Slug derived from the resolved page hierarchy.
|
||||
string derived_slug = 39;
|
||||
// Effective mount path after host inheritance and normalization.
|
||||
string mount_path = 40;
|
||||
}
|
||||
|
||||
@ -6670,6 +6670,7 @@ func (*ProvisionerEnsureSettingResponse) Descriptor() ([]byte, []int) {
|
||||
type PageSeed struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Slug string `protobuf:"bytes,1,opt,name=slug,proto3" json:"slug,omitempty"`
|
||||
// Legacy slug-based parent reference. Prefer parent_key for new page seeds.
|
||||
ParentSlug string `protobuf:"bytes,2,opt,name=parent_slug,json=parentSlug,proto3" json:"parent_slug,omitempty"`
|
||||
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
|
||||
TemplateKey string `protobuf:"bytes,4,opt,name=template_key,json=templateKey,proto3" json:"template_key,omitempty"`
|
||||
@ -6679,6 +6680,16 @@ type PageSeed struct {
|
||||
DetailSlugField string `protobuf:"bytes,8,opt,name=detail_slug_field,json=detailSlugField,proto3" json:"detail_slug_field,omitempty"`
|
||||
ReconcileBlocks bool `protobuf:"varint,9,opt,name=reconcile_blocks,json=reconcileBlocks,proto3" json:"reconcile_blocks,omitempty"`
|
||||
ReconcileTemplate bool `protobuf:"varint,10,opt,name=reconcile_template,json=reconcileTemplate,proto3" json:"reconcile_template,omitempty"`
|
||||
// Stable identity scoped to the declaring plugin.
|
||||
Key string `protobuf:"bytes,11,opt,name=key,proto3" json:"key,omitempty"`
|
||||
// Stable identity of a parent page declared by the same plugin.
|
||||
ParentKey string `protobuf:"bytes,12,opt,name=parent_key,json=parentKey,proto3" json:"parent_key,omitempty"`
|
||||
// Applied only when the page is created; later reconciliation does not move
|
||||
// an existing page when this value changes.
|
||||
MountPath string `protobuf:"bytes,13,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
|
||||
// Shallowly add absent top-level defaults to matching plugin-owned block
|
||||
// assignments while preserving all present values, including empty values.
|
||||
MergeMissingBlockContent bool `protobuf:"varint,14,opt,name=merge_missing_block_content,json=mergeMissingBlockContent,proto3" json:"merge_missing_block_content,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -6783,6 +6794,34 @@ func (x *PageSeed) GetReconcileTemplate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PageSeed) GetKey() string {
|
||||
if x != nil {
|
||||
return x.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageSeed) GetParentKey() string {
|
||||
if x != nil {
|
||||
return x.ParentKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageSeed) GetMountPath() string {
|
||||
if x != nil {
|
||||
return x.MountPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageSeed) GetMergeMissingBlockContent() bool {
|
||||
if x != nil {
|
||||
return x.MergeMissingBlockContent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ProvisionerEnsurePageRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Page *PageSeed `protobuf:"bytes,1,opt,name=page,proto3" json:"page,omitempty"`
|
||||
@ -6953,6 +6992,10 @@ type ProvisionerEnsureMenuItemRequest struct {
|
||||
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
|
||||
PageSlug string `protobuf:"bytes,4,opt,name=page_slug,json=pageSlug,proto3" json:"page_slug,omitempty"`
|
||||
SortOrder int32 `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"`
|
||||
// Stable plugin/page-key target. When set, these identify the provisioned
|
||||
// page independently of its current slug.
|
||||
PagePlugin string `protobuf:"bytes,6,opt,name=page_plugin,json=pagePlugin,proto3" json:"page_plugin,omitempty"`
|
||||
PageKey string `protobuf:"bytes,7,opt,name=page_key,json=pageKey,proto3" json:"page_key,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -7022,6 +7065,20 @@ func (x *ProvisionerEnsureMenuItemRequest) GetSortOrder() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ProvisionerEnsureMenuItemRequest) GetPagePlugin() string {
|
||||
if x != nil {
|
||||
return x.PagePlugin
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProvisionerEnsureMenuItemRequest) GetPageKey() string {
|
||||
if x != nil {
|
||||
return x.PageKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ProvisionerEnsureMenuItemResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
@ -8786,7 +8843,7 @@ const file_v1_capability_proto_rawDesc = "" +
|
||||
"\x1fProvisionerEnsureSettingRequest\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12,\n" +
|
||||
"\x12default_value_json\x18\x02 \x01(\fR\x10defaultValueJson\"\"\n" +
|
||||
" ProvisionerEnsureSettingResponse\"\x83\x03\n" +
|
||||
" ProvisionerEnsureSettingResponse\"\x92\x04\n" +
|
||||
"\bPageSeed\x12\x12\n" +
|
||||
"\x04slug\x18\x01 \x01(\tR\x04slug\x12\x1f\n" +
|
||||
"\vparent_slug\x18\x02 \x01(\tR\n" +
|
||||
@ -8799,20 +8856,29 @@ const file_v1_capability_proto_rawDesc = "" +
|
||||
"\x11detail_slug_field\x18\b \x01(\tR\x0fdetailSlugField\x12)\n" +
|
||||
"\x10reconcile_blocks\x18\t \x01(\bR\x0freconcileBlocks\x12-\n" +
|
||||
"\x12reconcile_template\x18\n" +
|
||||
" \x01(\bR\x11reconcileTemplate\"D\n" +
|
||||
" \x01(\bR\x11reconcileTemplate\x12\x10\n" +
|
||||
"\x03key\x18\v \x01(\tR\x03key\x12\x1d\n" +
|
||||
"\n" +
|
||||
"parent_key\x18\f \x01(\tR\tparentKey\x12\x1d\n" +
|
||||
"\n" +
|
||||
"mount_path\x18\r \x01(\tR\tmountPath\x12=\n" +
|
||||
"\x1bmerge_missing_block_content\x18\x0e \x01(\bR\x18mergeMissingBlockContent\"D\n" +
|
||||
"\x1cProvisionerEnsurePageRequest\x12$\n" +
|
||||
"\x04page\x18\x01 \x01(\v2\x10.abi.v1.PageSeedR\x04page\"\x1f\n" +
|
||||
"\x1dProvisionerEnsurePageResponse\"O\n" +
|
||||
"&ProvisionerOverrideSiteSettingsRequest\x12%\n" +
|
||||
"\x0eoverrides_json\x18\x01 \x01(\fR\roverridesJson\")\n" +
|
||||
"'ProvisionerOverrideSiteSettingsResponse\"\xa3\x01\n" +
|
||||
"'ProvisionerOverrideSiteSettingsResponse\"\xdf\x01\n" +
|
||||
" ProvisionerEnsureMenuItemRequest\x12\x1b\n" +
|
||||
"\tmenu_name\x18\x01 \x01(\tR\bmenuName\x12\x14\n" +
|
||||
"\x05label\x18\x02 \x01(\tR\x05label\x12\x10\n" +
|
||||
"\x03url\x18\x03 \x01(\tR\x03url\x12\x1b\n" +
|
||||
"\tpage_slug\x18\x04 \x01(\tR\bpageSlug\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sort_order\x18\x05 \x01(\x05R\tsortOrder\"#\n" +
|
||||
"sort_order\x18\x05 \x01(\x05R\tsortOrder\x12\x1f\n" +
|
||||
"\vpage_plugin\x18\x06 \x01(\tR\n" +
|
||||
"pagePlugin\x12\x19\n" +
|
||||
"\bpage_key\x18\a \x01(\tR\apageKey\"#\n" +
|
||||
"!ProvisionerEnsureMenuItemResponse\"\x87\x01\n" +
|
||||
")ProvisionerRegisterEmbeddingConfigRequest\x12\x1b\n" +
|
||||
"\ttable_key\x18\x01 \x01(\tR\btableKey\x12#\n" +
|
||||
|
||||
@ -1350,6 +1350,9 @@ type BridgeCallRequest struct {
|
||||
ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
|
||||
Method string `protobuf:"bytes,2,opt,name=method,proto3" json:"method,omitempty"`
|
||||
Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
// Host-authenticated identity of the consumer plugin. The host derives this
|
||||
// from the loaded caller; providers must not trust identity in payload.
|
||||
CallerPlugin string `protobuf:"bytes,4,opt,name=caller_plugin,json=callerPlugin,proto3" json:"caller_plugin,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -1405,6 +1408,13 @@ func (x *BridgeCallRequest) GetPayload() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *BridgeCallRequest) GetCallerPlugin() string {
|
||||
if x != nil {
|
||||
return x.CallerPlugin
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type BridgeCallResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
@ -1842,11 +1852,12 @@ const file_v1_invoke_proto_rawDesc = "" +
|
||||
"paramsJson\"S\n" +
|
||||
"\x12AiToolCallResponse\x12\x18\n" +
|
||||
"\acontent\x18\x01 \x01(\tR\acontent\x12#\n" +
|
||||
"\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"h\n" +
|
||||
"\rerror_message\x18\x02 \x01(\tR\ferrorMessage\"\x8d\x01\n" +
|
||||
"\x11BridgeCallRequest\x12!\n" +
|
||||
"\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x16\n" +
|
||||
"\x06method\x18\x02 \x01(\tR\x06method\x12\x18\n" +
|
||||
"\apayload\x18\x03 \x01(\fR\apayload\".\n" +
|
||||
"\apayload\x18\x03 \x01(\fR\apayload\x12#\n" +
|
||||
"\rcaller_plugin\x18\x04 \x01(\tR\fcallerPlugin\".\n" +
|
||||
"\x12BridgeCallResponse\x12\x18\n" +
|
||||
"\apayload\x18\x01 \x01(\fR\apayload\"Q\n" +
|
||||
"\x1cDirectoryPanelSectionRequest\x12\x14\n" +
|
||||
|
||||
@ -162,6 +162,13 @@ type PoweredBlock struct {
|
||||
Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
|
||||
// JSON encoding of the template data map (map[string]any).
|
||||
DataJson []byte `protobuf:"bytes,2,opt,name=data_json,json=dataJson,proto3" json:"data_json,omitempty"`
|
||||
// Requested HTTP response status. Before writing headers, the host accepts
|
||||
// only 200–599, uses the highest status class, and keeps the first result
|
||||
// encountered within that class.
|
||||
StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
|
||||
// Structured document metadata merged by the host into the page head. The
|
||||
// first non-empty value for each field wins in deterministic render order.
|
||||
Metadata *DocumentMetadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -210,6 +217,98 @@ func (x *PoweredBlock) GetDataJson() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PoweredBlock) GetStatusCode() int32 {
|
||||
if x != nil {
|
||||
return x.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PoweredBlock) GetMetadata() *DocumentMetadata {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DocumentMetadata is the safe, shared subset of document head data a plugin
|
||||
// may provide. The host validates and escapes every value before rendering.
|
||||
type DocumentMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||
CanonicalUrl string `protobuf:"bytes,3,opt,name=canonical_url,json=canonicalUrl,proto3" json:"canonical_url,omitempty"`
|
||||
ImageUrl string `protobuf:"bytes,4,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"`
|
||||
Robots string `protobuf:"bytes,5,opt,name=robots,proto3" json:"robots,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) Reset() {
|
||||
*x = DocumentMetadata{}
|
||||
mi := &file_v1_render_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DocumentMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *DocumentMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DocumentMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*DocumentMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) GetTitle() string {
|
||||
if x != nil {
|
||||
return x.Title
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) GetDescription() string {
|
||||
if x != nil {
|
||||
return x.Description
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) GetCanonicalUrl() string {
|
||||
if x != nil {
|
||||
return x.CanonicalUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) GetImageUrl() string {
|
||||
if x != nil {
|
||||
return x.ImageUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DocumentMetadata) GetRobots() string {
|
||||
if x != nil {
|
||||
return x.Robots
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RenderTemplateRequest renders one template (templates.TemplateFunc).
|
||||
type RenderTemplateRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -223,7 +322,7 @@ type RenderTemplateRequest struct {
|
||||
|
||||
func (x *RenderTemplateRequest) Reset() {
|
||||
*x = RenderTemplateRequest{}
|
||||
mi := &file_v1_render_proto_msgTypes[3]
|
||||
mi := &file_v1_render_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -235,7 +334,7 @@ func (x *RenderTemplateRequest) String() string {
|
||||
func (*RenderTemplateRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[3]
|
||||
mi := &file_v1_render_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -248,7 +347,7 @@ func (x *RenderTemplateRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RenderTemplateRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RenderTemplateRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{3}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *RenderTemplateRequest) GetTemplateKey() string {
|
||||
@ -281,7 +380,7 @@ type RenderTemplateResponse struct {
|
||||
|
||||
func (x *RenderTemplateResponse) Reset() {
|
||||
*x = RenderTemplateResponse{}
|
||||
mi := &file_v1_render_proto_msgTypes[4]
|
||||
mi := &file_v1_render_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -293,7 +392,7 @@ func (x *RenderTemplateResponse) String() string {
|
||||
func (*RenderTemplateResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[4]
|
||||
mi := &file_v1_render_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -306,7 +405,7 @@ func (x *RenderTemplateResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RenderTemplateResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RenderTemplateResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{4}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *RenderTemplateResponse) GetDocument() *TemplateDocument {
|
||||
@ -330,13 +429,18 @@ type TemplateDocument struct {
|
||||
// Extra <html>-element attributes (e.g. data-theme). A "lang" key here is
|
||||
// ignored — the lang field wins.
|
||||
HtmlAttrs map[string]string `protobuf:"bytes,6,rep,name=html_attrs,json=htmlAttrs,proto3" json:"html_attrs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
// Requested HTTP response status. The host applies the same validation and
|
||||
// precedence rules as PoweredBlock.status_code.
|
||||
StatusCode int32 `protobuf:"varint,7,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
|
||||
// Structured document metadata merged by the host into the page head.
|
||||
Metadata *DocumentMetadata `protobuf:"bytes,8,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TemplateDocument) Reset() {
|
||||
*x = TemplateDocument{}
|
||||
mi := &file_v1_render_proto_msgTypes[5]
|
||||
mi := &file_v1_render_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -348,7 +452,7 @@ func (x *TemplateDocument) String() string {
|
||||
func (*TemplateDocument) ProtoMessage() {}
|
||||
|
||||
func (x *TemplateDocument) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[5]
|
||||
mi := &file_v1_render_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -361,7 +465,7 @@ func (x *TemplateDocument) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use TemplateDocument.ProtoReflect.Descriptor instead.
|
||||
func (*TemplateDocument) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{5}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *TemplateDocument) GetBodyHtml() []byte {
|
||||
@ -406,6 +510,20 @@ func (x *TemplateDocument) GetHtmlAttrs() map[string]string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TemplateDocument) GetStatusCode() int32 {
|
||||
if x != nil {
|
||||
return x.StatusCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TemplateDocument) GetMetadata() *DocumentMetadata {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderTagRequest invokes a plugin-declared template tag (HOOK_RENDER_TAG).
|
||||
// The host wires one pongo2 tag per manifest.declared_tags name; when its
|
||||
// engine hits that tag while rendering a powered block, it calls back here.
|
||||
@ -424,7 +542,7 @@ type RenderTagRequest struct {
|
||||
|
||||
func (x *RenderTagRequest) Reset() {
|
||||
*x = RenderTagRequest{}
|
||||
mi := &file_v1_render_proto_msgTypes[6]
|
||||
mi := &file_v1_render_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -436,7 +554,7 @@ func (x *RenderTagRequest) String() string {
|
||||
func (*RenderTagRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RenderTagRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[6]
|
||||
mi := &file_v1_render_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -449,7 +567,7 @@ func (x *RenderTagRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RenderTagRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RenderTagRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{6}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *RenderTagRequest) GetTagName() string {
|
||||
@ -486,7 +604,7 @@ type RenderTagResponse struct {
|
||||
|
||||
func (x *RenderTagResponse) Reset() {
|
||||
*x = RenderTagResponse{}
|
||||
mi := &file_v1_render_proto_msgTypes[7]
|
||||
mi := &file_v1_render_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -498,7 +616,7 @@ func (x *RenderTagResponse) String() string {
|
||||
func (*RenderTagResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RenderTagResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[7]
|
||||
mi := &file_v1_render_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -511,7 +629,7 @@ func (x *RenderTagResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RenderTagResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RenderTagResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{7}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *RenderTagResponse) GetHtml() string {
|
||||
@ -544,7 +662,7 @@ type ApplyFilterRequest struct {
|
||||
|
||||
func (x *ApplyFilterRequest) Reset() {
|
||||
*x = ApplyFilterRequest{}
|
||||
mi := &file_v1_render_proto_msgTypes[8]
|
||||
mi := &file_v1_render_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -556,7 +674,7 @@ func (x *ApplyFilterRequest) String() string {
|
||||
func (*ApplyFilterRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[8]
|
||||
mi := &file_v1_render_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -569,7 +687,7 @@ func (x *ApplyFilterRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ApplyFilterRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ApplyFilterRequest) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{8}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *ApplyFilterRequest) GetFilterName() string {
|
||||
@ -605,7 +723,7 @@ type ApplyFilterResponse struct {
|
||||
|
||||
func (x *ApplyFilterResponse) Reset() {
|
||||
*x = ApplyFilterResponse{}
|
||||
mi := &file_v1_render_proto_msgTypes[9]
|
||||
mi := &file_v1_render_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -617,7 +735,7 @@ func (x *ApplyFilterResponse) String() string {
|
||||
func (*ApplyFilterResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[9]
|
||||
mi := &file_v1_render_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -630,7 +748,7 @@ func (x *ApplyFilterResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use ApplyFilterResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ApplyFilterResponse) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{9}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *ApplyFilterResponse) GetOutput() string {
|
||||
@ -694,7 +812,7 @@ type RenderContext struct {
|
||||
|
||||
func (x *RenderContext) Reset() {
|
||||
*x = RenderContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[10]
|
||||
mi := &file_v1_render_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -706,7 +824,7 @@ func (x *RenderContext) String() string {
|
||||
func (*RenderContext) ProtoMessage() {}
|
||||
|
||||
func (x *RenderContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[10]
|
||||
mi := &file_v1_render_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -719,7 +837,7 @@ func (x *RenderContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RenderContext.ProtoReflect.Descriptor instead.
|
||||
func (*RenderContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{10}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *RenderContext) GetRequest() *RequestInfo {
|
||||
@ -868,7 +986,7 @@ type RequestInfo struct {
|
||||
|
||||
func (x *RequestInfo) Reset() {
|
||||
*x = RequestInfo{}
|
||||
mi := &file_v1_render_proto_msgTypes[11]
|
||||
mi := &file_v1_render_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -880,7 +998,7 @@ func (x *RequestInfo) String() string {
|
||||
func (*RequestInfo) ProtoMessage() {}
|
||||
|
||||
func (x *RequestInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[11]
|
||||
mi := &file_v1_render_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -893,7 +1011,7 @@ func (x *RequestInfo) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead.
|
||||
func (*RequestInfo) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{11}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *RequestInfo) GetMethod() string {
|
||||
@ -974,13 +1092,17 @@ type PageContext struct {
|
||||
Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
|
||||
PostType string `protobuf:"bytes,4,opt,name=post_type,json=postType,proto3" json:"post_type,omitempty"` // "page", "post", "master", "system"
|
||||
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "published", "draft", "scheduled"
|
||||
// Slug derived from the resolved page hierarchy.
|
||||
DerivedSlug string `protobuf:"bytes,6,opt,name=derived_slug,json=derivedSlug,proto3" json:"derived_slug,omitempty"`
|
||||
// Effective mount path after host inheritance and normalization.
|
||||
MountPath string `protobuf:"bytes,7,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PageContext) Reset() {
|
||||
*x = PageContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[12]
|
||||
mi := &file_v1_render_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -992,7 +1114,7 @@ func (x *PageContext) String() string {
|
||||
func (*PageContext) ProtoMessage() {}
|
||||
|
||||
func (x *PageContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[12]
|
||||
mi := &file_v1_render_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1005,7 +1127,7 @@ func (x *PageContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use PageContext.ProtoReflect.Descriptor instead.
|
||||
func (*PageContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{12}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *PageContext) GetId() string {
|
||||
@ -1043,6 +1165,20 @@ func (x *PageContext) GetStatus() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageContext) GetDerivedSlug() string {
|
||||
if x != nil {
|
||||
return x.DerivedSlug
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageContext) GetMountPath() string {
|
||||
if x != nil {
|
||||
return x.MountPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PostContext mirrors blocks.PostContext.
|
||||
type PostContext struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -1061,7 +1197,7 @@ type PostContext struct {
|
||||
|
||||
func (x *PostContext) Reset() {
|
||||
*x = PostContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[13]
|
||||
mi := &file_v1_render_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1073,7 +1209,7 @@ func (x *PostContext) String() string {
|
||||
func (*PostContext) ProtoMessage() {}
|
||||
|
||||
func (x *PostContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[13]
|
||||
mi := &file_v1_render_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1086,7 +1222,7 @@ func (x *PostContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use PostContext.ProtoReflect.Descriptor instead.
|
||||
func (*PostContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{13}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *PostContext) GetId() string {
|
||||
@ -1166,7 +1302,7 @@ type AuthorContext struct {
|
||||
|
||||
func (x *AuthorContext) Reset() {
|
||||
*x = AuthorContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[14]
|
||||
mi := &file_v1_render_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1178,7 +1314,7 @@ func (x *AuthorContext) String() string {
|
||||
func (*AuthorContext) ProtoMessage() {}
|
||||
|
||||
func (x *AuthorContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[14]
|
||||
mi := &file_v1_render_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1191,7 +1327,7 @@ func (x *AuthorContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use AuthorContext.ProtoReflect.Descriptor instead.
|
||||
func (*AuthorContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{14}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *AuthorContext) GetId() string {
|
||||
@ -1241,7 +1377,7 @@ type CategoryContext struct {
|
||||
|
||||
func (x *CategoryContext) Reset() {
|
||||
*x = CategoryContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[15]
|
||||
mi := &file_v1_render_proto_msgTypes[16]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1253,7 +1389,7 @@ func (x *CategoryContext) String() string {
|
||||
func (*CategoryContext) ProtoMessage() {}
|
||||
|
||||
func (x *CategoryContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[15]
|
||||
mi := &file_v1_render_proto_msgTypes[16]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1266,7 +1402,7 @@ func (x *CategoryContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CategoryContext.ProtoReflect.Descriptor instead.
|
||||
func (*CategoryContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{15}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{16}
|
||||
}
|
||||
|
||||
func (x *CategoryContext) GetId() string {
|
||||
@ -1302,7 +1438,7 @@ type MasterPageContext struct {
|
||||
|
||||
func (x *MasterPageContext) Reset() {
|
||||
*x = MasterPageContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[16]
|
||||
mi := &file_v1_render_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1314,7 +1450,7 @@ func (x *MasterPageContext) String() string {
|
||||
func (*MasterPageContext) ProtoMessage() {}
|
||||
|
||||
func (x *MasterPageContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[16]
|
||||
mi := &file_v1_render_proto_msgTypes[17]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1327,7 +1463,7 @@ func (x *MasterPageContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use MasterPageContext.ProtoReflect.Descriptor instead.
|
||||
func (*MasterPageContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{16}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *MasterPageContext) GetId() string {
|
||||
@ -1364,7 +1500,7 @@ type HumanProofBanner struct {
|
||||
|
||||
func (x *HumanProofBanner) Reset() {
|
||||
*x = HumanProofBanner{}
|
||||
mi := &file_v1_render_proto_msgTypes[17]
|
||||
mi := &file_v1_render_proto_msgTypes[18]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1376,7 +1512,7 @@ func (x *HumanProofBanner) String() string {
|
||||
func (*HumanProofBanner) ProtoMessage() {}
|
||||
|
||||
func (x *HumanProofBanner) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[17]
|
||||
mi := &file_v1_render_proto_msgTypes[18]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1389,7 +1525,7 @@ func (x *HumanProofBanner) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use HumanProofBanner.ProtoReflect.Descriptor instead.
|
||||
func (*HumanProofBanner) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{17}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *HumanProofBanner) GetActiveTimeMinutes() int32 {
|
||||
@ -1433,7 +1569,7 @@ type DetailRow struct {
|
||||
|
||||
func (x *DetailRow) Reset() {
|
||||
*x = DetailRow{}
|
||||
mi := &file_v1_render_proto_msgTypes[18]
|
||||
mi := &file_v1_render_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1445,7 +1581,7 @@ func (x *DetailRow) String() string {
|
||||
func (*DetailRow) ProtoMessage() {}
|
||||
|
||||
func (x *DetailRow) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[18]
|
||||
mi := &file_v1_render_proto_msgTypes[19]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1458,7 +1594,7 @@ func (x *DetailRow) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use DetailRow.ProtoReflect.Descriptor instead.
|
||||
func (*DetailRow) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{18}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *DetailRow) GetTableId() string {
|
||||
@ -1524,13 +1660,17 @@ type BlockContext struct {
|
||||
DetailRowDataJson []byte `protobuf:"bytes,36,opt,name=detail_row_data_json,json=detailRowDataJson,proto3" json:"detail_row_data_json,omitempty"`
|
||||
BlogIndexUrl string `protobuf:"bytes,37,opt,name=blog_index_url,json=blogIndexUrl,proto3" json:"blog_index_url,omitempty"`
|
||||
CategoryPageUrl string `protobuf:"bytes,38,opt,name=category_page_url,json=categoryPageUrl,proto3" json:"category_page_url,omitempty"`
|
||||
// Slug derived from the resolved page hierarchy.
|
||||
DerivedSlug string `protobuf:"bytes,39,opt,name=derived_slug,json=derivedSlug,proto3" json:"derived_slug,omitempty"`
|
||||
// Effective mount path after host inheritance and normalization.
|
||||
MountPath string `protobuf:"bytes,40,opt,name=mount_path,json=mountPath,proto3" json:"mount_path,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BlockContext) Reset() {
|
||||
*x = BlockContext{}
|
||||
mi := &file_v1_render_proto_msgTypes[19]
|
||||
mi := &file_v1_render_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -1542,7 +1682,7 @@ func (x *BlockContext) String() string {
|
||||
func (*BlockContext) ProtoMessage() {}
|
||||
|
||||
func (x *BlockContext) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v1_render_proto_msgTypes[19]
|
||||
mi := &file_v1_render_proto_msgTypes[20]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -1555,7 +1695,7 @@ func (x *BlockContext) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use BlockContext.ProtoReflect.Descriptor instead.
|
||||
func (*BlockContext) Descriptor() ([]byte, []int) {
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{19}
|
||||
return file_v1_render_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *BlockContext) GetUrl() string {
|
||||
@ -1824,6 +1964,20 @@ func (x *BlockContext) GetCategoryPageUrl() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BlockContext) GetDerivedSlug() string {
|
||||
if x != nil {
|
||||
return x.DerivedSlug
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BlockContext) GetMountPath() string {
|
||||
if x != nil {
|
||||
return x.MountPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_v1_render_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_v1_render_proto_rawDesc = "" +
|
||||
@ -1835,16 +1989,25 @@ const file_v1_render_proto_rawDesc = "" +
|
||||
"\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"Y\n" +
|
||||
"\x13RenderBlockResponse\x12\x12\n" +
|
||||
"\x04html\x18\x01 \x01(\tR\x04html\x12.\n" +
|
||||
"\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"G\n" +
|
||||
"\apowered\x18\x02 \x01(\v2\x14.abi.v1.PoweredBlockR\apowered\"\x9e\x01\n" +
|
||||
"\fPoweredBlock\x12\x1a\n" +
|
||||
"\btemplate\x18\x01 \x01(\tR\btemplate\x12\x1b\n" +
|
||||
"\tdata_json\x18\x02 \x01(\fR\bdataJson\"\x93\x01\n" +
|
||||
"\tdata_json\x18\x02 \x01(\fR\bdataJson\x12\x1f\n" +
|
||||
"\vstatus_code\x18\x03 \x01(\x05R\n" +
|
||||
"statusCode\x124\n" +
|
||||
"\bmetadata\x18\x04 \x01(\v2\x18.abi.v1.DocumentMetadataR\bmetadata\"\xa4\x01\n" +
|
||||
"\x10DocumentMetadata\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
|
||||
"\vdescription\x18\x02 \x01(\tR\vdescription\x12#\n" +
|
||||
"\rcanonical_url\x18\x03 \x01(\tR\fcanonicalUrl\x12\x1b\n" +
|
||||
"\timage_url\x18\x04 \x01(\tR\bimageUrl\x12\x16\n" +
|
||||
"\x06robots\x18\x05 \x01(\tR\x06robots\"\x93\x01\n" +
|
||||
"\x15RenderTemplateRequest\x12!\n" +
|
||||
"\ftemplate_key\x18\x01 \x01(\tR\vtemplateKey\x12\x19\n" +
|
||||
"\bdoc_json\x18\x02 \x01(\fR\adocJson\x12<\n" +
|
||||
"\x0erender_context\x18\x03 \x01(\v2\x15.abi.v1.RenderContextR\rrenderContext\"T\n" +
|
||||
"\x16RenderTemplateResponse\x124\n" +
|
||||
"\bdocument\x18\x02 \x01(\v2\x18.abi.v1.TemplateDocumentR\bdocumentJ\x04\b\x01\x10\x02\"\xbf\x02\n" +
|
||||
"\bdocument\x18\x02 \x01(\v2\x18.abi.v1.TemplateDocumentR\bdocumentJ\x04\b\x01\x10\x02\"\x96\x03\n" +
|
||||
"\x10TemplateDocument\x12\x1b\n" +
|
||||
"\tbody_html\x18\x01 \x01(\fR\bbodyHtml\x12\x1d\n" +
|
||||
"\n" +
|
||||
@ -1853,7 +2016,10 @@ const file_v1_render_proto_rawDesc = "" +
|
||||
"\x0fhead_extra_html\x18\x04 \x01(\fR\rheadExtraHtml\x12-\n" +
|
||||
"\x13body_end_extra_html\x18\x05 \x01(\fR\x10bodyEndExtraHtml\x12F\n" +
|
||||
"\n" +
|
||||
"html_attrs\x18\x06 \x03(\v2'.abi.v1.TemplateDocument.HtmlAttrsEntryR\thtmlAttrs\x1a<\n" +
|
||||
"html_attrs\x18\x06 \x03(\v2'.abi.v1.TemplateDocument.HtmlAttrsEntryR\thtmlAttrs\x12\x1f\n" +
|
||||
"\vstatus_code\x18\a \x01(\x05R\n" +
|
||||
"statusCode\x124\n" +
|
||||
"\bmetadata\x18\b \x01(\v2\x18.abi.v1.DocumentMetadataR\bmetadata\x1a<\n" +
|
||||
"\x0eHtmlAttrsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x88\x01\n" +
|
||||
@ -1916,13 +2082,16 @@ const file_v1_render_proto_rawDesc = "" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" +
|
||||
"\fCookiesEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"|\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbe\x01\n" +
|
||||
"\vPageContext\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" +
|
||||
"\x05title\x18\x03 \x01(\tR\x05title\x12\x1b\n" +
|
||||
"\tpost_type\x18\x04 \x01(\tR\bpostType\x12\x16\n" +
|
||||
"\x06status\x18\x05 \x01(\tR\x06status\"\xaf\x02\n" +
|
||||
"\x06status\x18\x05 \x01(\tR\x06status\x12!\n" +
|
||||
"\fderived_slug\x18\x06 \x01(\tR\vderivedSlug\x12\x1d\n" +
|
||||
"\n" +
|
||||
"mount_path\x18\a \x01(\tR\tmountPath\"\xaf\x02\n" +
|
||||
"\vPostContext\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04slug\x18\x02 \x01(\tR\x04slug\x12\x14\n" +
|
||||
@ -1957,7 +2126,7 @@ const file_v1_render_proto_rawDesc = "" +
|
||||
"\tDetailRow\x12\x19\n" +
|
||||
"\btable_id\x18\x01 \x01(\tR\atableId\x12\x15\n" +
|
||||
"\x06row_id\x18\x02 \x01(\tR\x05rowId\x12\x1b\n" +
|
||||
"\tdata_json\x18\x03 \x01(\fR\bdataJson\"\x85\f\n" +
|
||||
"\tdata_json\x18\x03 \x01(\fR\bdataJson\"\xc7\f\n" +
|
||||
"\fBlockContext\x12\x10\n" +
|
||||
"\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
|
||||
"\x04path\x18\x02 \x01(\tR\x04path\x12\x12\n" +
|
||||
@ -2001,7 +2170,10 @@ const file_v1_render_proto_rawDesc = "" +
|
||||
"\x0fdetail_table_id\x18# \x01(\tR\rdetailTableId\x12/\n" +
|
||||
"\x14detail_row_data_json\x18$ \x01(\fR\x11detailRowDataJson\x12$\n" +
|
||||
"\x0eblog_index_url\x18% \x01(\tR\fblogIndexUrl\x12*\n" +
|
||||
"\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x1a8\n" +
|
||||
"\x11category_page_url\x18& \x01(\tR\x0fcategoryPageUrl\x12!\n" +
|
||||
"\fderived_slug\x18' \x01(\tR\vderivedSlug\x12\x1d\n" +
|
||||
"\n" +
|
||||
"mount_path\x18( \x01(\tR\tmountPath\x1a8\n" +
|
||||
"\n" +
|
||||
"QueryEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
@ -2025,66 +2197,69 @@ func file_v1_render_proto_rawDescGZIP() []byte {
|
||||
return file_v1_render_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
|
||||
var file_v1_render_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
|
||||
var file_v1_render_proto_goTypes = []any{
|
||||
(*RenderBlockRequest)(nil), // 0: abi.v1.RenderBlockRequest
|
||||
(*RenderBlockResponse)(nil), // 1: abi.v1.RenderBlockResponse
|
||||
(*PoweredBlock)(nil), // 2: abi.v1.PoweredBlock
|
||||
(*RenderTemplateRequest)(nil), // 3: abi.v1.RenderTemplateRequest
|
||||
(*RenderTemplateResponse)(nil), // 4: abi.v1.RenderTemplateResponse
|
||||
(*TemplateDocument)(nil), // 5: abi.v1.TemplateDocument
|
||||
(*RenderTagRequest)(nil), // 6: abi.v1.RenderTagRequest
|
||||
(*RenderTagResponse)(nil), // 7: abi.v1.RenderTagResponse
|
||||
(*ApplyFilterRequest)(nil), // 8: abi.v1.ApplyFilterRequest
|
||||
(*ApplyFilterResponse)(nil), // 9: abi.v1.ApplyFilterResponse
|
||||
(*RenderContext)(nil), // 10: abi.v1.RenderContext
|
||||
(*RequestInfo)(nil), // 11: abi.v1.RequestInfo
|
||||
(*PageContext)(nil), // 12: abi.v1.PageContext
|
||||
(*PostContext)(nil), // 13: abi.v1.PostContext
|
||||
(*AuthorContext)(nil), // 14: abi.v1.AuthorContext
|
||||
(*CategoryContext)(nil), // 15: abi.v1.CategoryContext
|
||||
(*MasterPageContext)(nil), // 16: abi.v1.MasterPageContext
|
||||
(*HumanProofBanner)(nil), // 17: abi.v1.HumanProofBanner
|
||||
(*DetailRow)(nil), // 18: abi.v1.DetailRow
|
||||
(*BlockContext)(nil), // 19: abi.v1.BlockContext
|
||||
nil, // 20: abi.v1.TemplateDocument.HtmlAttrsEntry
|
||||
nil, // 21: abi.v1.RenderContext.InjectedSlotsEntry
|
||||
nil, // 22: abi.v1.RequestInfo.HeadersEntry
|
||||
nil, // 23: abi.v1.RequestInfo.CookiesEntry
|
||||
nil, // 24: abi.v1.BlockContext.QueryEntry
|
||||
nil, // 25: abi.v1.BlockContext.CookiesEntry
|
||||
nil, // 26: abi.v1.BlockContext.HeadersEntry
|
||||
(*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
|
||||
(*DocumentMetadata)(nil), // 3: abi.v1.DocumentMetadata
|
||||
(*RenderTemplateRequest)(nil), // 4: abi.v1.RenderTemplateRequest
|
||||
(*RenderTemplateResponse)(nil), // 5: abi.v1.RenderTemplateResponse
|
||||
(*TemplateDocument)(nil), // 6: abi.v1.TemplateDocument
|
||||
(*RenderTagRequest)(nil), // 7: abi.v1.RenderTagRequest
|
||||
(*RenderTagResponse)(nil), // 8: abi.v1.RenderTagResponse
|
||||
(*ApplyFilterRequest)(nil), // 9: abi.v1.ApplyFilterRequest
|
||||
(*ApplyFilterResponse)(nil), // 10: abi.v1.ApplyFilterResponse
|
||||
(*RenderContext)(nil), // 11: abi.v1.RenderContext
|
||||
(*RequestInfo)(nil), // 12: abi.v1.RequestInfo
|
||||
(*PageContext)(nil), // 13: abi.v1.PageContext
|
||||
(*PostContext)(nil), // 14: abi.v1.PostContext
|
||||
(*AuthorContext)(nil), // 15: abi.v1.AuthorContext
|
||||
(*CategoryContext)(nil), // 16: abi.v1.CategoryContext
|
||||
(*MasterPageContext)(nil), // 17: abi.v1.MasterPageContext
|
||||
(*HumanProofBanner)(nil), // 18: abi.v1.HumanProofBanner
|
||||
(*DetailRow)(nil), // 19: abi.v1.DetailRow
|
||||
(*BlockContext)(nil), // 20: abi.v1.BlockContext
|
||||
nil, // 21: abi.v1.TemplateDocument.HtmlAttrsEntry
|
||||
nil, // 22: abi.v1.RenderContext.InjectedSlotsEntry
|
||||
nil, // 23: abi.v1.RequestInfo.HeadersEntry
|
||||
nil, // 24: abi.v1.RequestInfo.CookiesEntry
|
||||
nil, // 25: abi.v1.BlockContext.QueryEntry
|
||||
nil, // 26: abi.v1.BlockContext.CookiesEntry
|
||||
nil, // 27: abi.v1.BlockContext.HeadersEntry
|
||||
(*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
|
||||
}
|
||||
var file_v1_render_proto_depIdxs = []int32{
|
||||
10, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext
|
||||
11, // 0: abi.v1.RenderBlockRequest.render_context:type_name -> abi.v1.RenderContext
|
||||
2, // 1: abi.v1.RenderBlockResponse.powered:type_name -> abi.v1.PoweredBlock
|
||||
10, // 2: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext
|
||||
5, // 3: abi.v1.RenderTemplateResponse.document:type_name -> abi.v1.TemplateDocument
|
||||
20, // 4: abi.v1.TemplateDocument.html_attrs:type_name -> abi.v1.TemplateDocument.HtmlAttrsEntry
|
||||
10, // 5: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext
|
||||
11, // 6: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo
|
||||
19, // 7: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext
|
||||
12, // 8: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext
|
||||
13, // 9: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext
|
||||
14, // 10: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext
|
||||
15, // 11: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext
|
||||
16, // 12: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext
|
||||
21, // 13: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry
|
||||
17, // 14: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner
|
||||
18, // 15: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow
|
||||
22, // 16: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry
|
||||
23, // 17: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry
|
||||
27, // 18: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp
|
||||
27, // 19: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp
|
||||
24, // 20: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry
|
||||
25, // 21: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry
|
||||
26, // 22: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry
|
||||
23, // [23:23] is the sub-list for method output_type
|
||||
23, // [23:23] is the sub-list for method input_type
|
||||
23, // [23:23] is the sub-list for extension type_name
|
||||
23, // [23:23] is the sub-list for extension extendee
|
||||
0, // [0:23] is the sub-list for field type_name
|
||||
3, // 2: abi.v1.PoweredBlock.metadata:type_name -> abi.v1.DocumentMetadata
|
||||
11, // 3: abi.v1.RenderTemplateRequest.render_context:type_name -> abi.v1.RenderContext
|
||||
6, // 4: abi.v1.RenderTemplateResponse.document:type_name -> abi.v1.TemplateDocument
|
||||
21, // 5: abi.v1.TemplateDocument.html_attrs:type_name -> abi.v1.TemplateDocument.HtmlAttrsEntry
|
||||
3, // 6: abi.v1.TemplateDocument.metadata:type_name -> abi.v1.DocumentMetadata
|
||||
11, // 7: abi.v1.RenderTagRequest.render_context:type_name -> abi.v1.RenderContext
|
||||
12, // 8: abi.v1.RenderContext.request:type_name -> abi.v1.RequestInfo
|
||||
20, // 9: abi.v1.RenderContext.block_context:type_name -> abi.v1.BlockContext
|
||||
13, // 10: abi.v1.RenderContext.page:type_name -> abi.v1.PageContext
|
||||
14, // 11: abi.v1.RenderContext.post:type_name -> abi.v1.PostContext
|
||||
15, // 12: abi.v1.RenderContext.author:type_name -> abi.v1.AuthorContext
|
||||
16, // 13: abi.v1.RenderContext.category:type_name -> abi.v1.CategoryContext
|
||||
17, // 14: abi.v1.RenderContext.master_page:type_name -> abi.v1.MasterPageContext
|
||||
22, // 15: abi.v1.RenderContext.injected_slots:type_name -> abi.v1.RenderContext.InjectedSlotsEntry
|
||||
18, // 16: abi.v1.RenderContext.human_proof_banner:type_name -> abi.v1.HumanProofBanner
|
||||
19, // 17: abi.v1.RenderContext.detail_row:type_name -> abi.v1.DetailRow
|
||||
23, // 18: abi.v1.RequestInfo.headers:type_name -> abi.v1.RequestInfo.HeadersEntry
|
||||
24, // 19: abi.v1.RequestInfo.cookies:type_name -> abi.v1.RequestInfo.CookiesEntry
|
||||
28, // 20: abi.v1.PostContext.published_at:type_name -> google.protobuf.Timestamp
|
||||
28, // 21: abi.v1.BlockContext.now:type_name -> google.protobuf.Timestamp
|
||||
25, // 22: abi.v1.BlockContext.query:type_name -> abi.v1.BlockContext.QueryEntry
|
||||
26, // 23: abi.v1.BlockContext.cookies:type_name -> abi.v1.BlockContext.CookiesEntry
|
||||
27, // 24: abi.v1.BlockContext.headers:type_name -> abi.v1.BlockContext.HeadersEntry
|
||||
25, // [25:25] is the sub-list for method output_type
|
||||
25, // [25:25] is the sub-list for method input_type
|
||||
25, // [25:25] is the sub-list for extension type_name
|
||||
25, // [25:25] is the sub-list for extension extendee
|
||||
0, // [0:25] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_v1_render_proto_init() }
|
||||
@ -2098,7 +2273,7 @@ func file_v1_render_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_v1_render_proto_rawDesc), len(file_v1_render_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 27,
|
||||
NumMessages: 28,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -97,10 +98,8 @@ func TestCaptchaVerifiedReadsTrustedHeader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAllTrustedHeadersIncludesCaptcha(t *testing.T) {
|
||||
for _, h := range AllTrustedHeaders() {
|
||||
if h == HeaderVerifiedCaptcha {
|
||||
if slices.Contains(AllTrustedHeaders(), HeaderVerifiedCaptcha) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("AllTrustedHeaders() missing %s — hosts strip-then-stamp from this list, so omitting it makes the header client-forgeable", HeaderVerifiedCaptcha)
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ import (
|
||||
type PageContext struct {
|
||||
ID uuid.UUID
|
||||
Slug string
|
||||
DerivedSlug string // resolved route from the page hierarchy
|
||||
MountPath string // effective host-resolved mount prefix
|
||||
Title string
|
||||
PostType string // "page", "post", "master", "system"
|
||||
Status string // "published", "draft", "scheduled"
|
||||
@ -371,6 +373,8 @@ type BlockContext struct {
|
||||
URL string `pongo2:"url"`
|
||||
Path string `pongo2:"path"`
|
||||
Slug string `pongo2:"slug"`
|
||||
DerivedSlug string `pongo2:"derivedSlug"`
|
||||
MountPath string `pongo2:"mountPath"`
|
||||
PageID string `pongo2:"pageId"`
|
||||
PageTitle string `pongo2:"pageTitle"`
|
||||
TemplateKey string `pongo2:"templateKey"`
|
||||
@ -425,6 +429,7 @@ func GetBlockContext(ctx context.Context) *BlockContext {
|
||||
func (bc *BlockContext) ToMap() map[string]any {
|
||||
return map[string]any{
|
||||
"url": bc.URL, "path": bc.Path, "slug": bc.Slug,
|
||||
"derivedSlug": bc.DerivedSlug, "mountPath": bc.MountPath,
|
||||
"pageId": bc.PageID, "pageTitle": bc.PageTitle,
|
||||
"templateKey": bc.TemplateKey, "isEditor": bc.IsEditor,
|
||||
"timestamp": bc.Timestamp, "now": bc.Now,
|
||||
|
||||
@ -3,6 +3,8 @@ package blocks
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
)
|
||||
|
||||
// poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL
|
||||
@ -18,6 +20,15 @@ const poweredBlockSentinel = "\x00bn:powered\x00"
|
||||
type PoweredResult struct {
|
||||
Template string `json:"template"`
|
||||
Data map[string]any `json:"data"`
|
||||
StatusCode int32 `json:"statusCode,omitempty"`
|
||||
Metadata *document.Metadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// PoweredOptions controls host-owned response data for a powered block.
|
||||
// StatusCode is transported as declared; the host accepts only 200–599.
|
||||
type PoweredOptions struct {
|
||||
StatusCode int32
|
||||
Metadata *document.Metadata
|
||||
}
|
||||
|
||||
// PoweredBlock marks a block's return value as "powered": instead of final
|
||||
@ -37,11 +48,28 @@ type PoweredResult struct {
|
||||
// hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke,
|
||||
// never a re-entrant one.
|
||||
func PoweredBlock(template string, data map[string]any) string {
|
||||
payload, err := json.Marshal(PoweredResult{Template: template, Data: data})
|
||||
return poweredBlockMarker(PoweredResult{Template: template, Data: data})
|
||||
}
|
||||
|
||||
// PoweredBlockWithOptions is PoweredBlock with structured document metadata
|
||||
// and an optional HTTP response status for the host to accumulate before it
|
||||
// writes response headers.
|
||||
func PoweredBlockWithOptions(template string, data map[string]any, options PoweredOptions) string {
|
||||
return poweredBlockMarker(PoweredResult{
|
||||
Template: template,
|
||||
Data: data,
|
||||
StatusCode: options.StatusCode,
|
||||
Metadata: options.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func poweredBlockMarker(result PoweredResult) string {
|
||||
payload, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
// A non-serializable data map is a programming error; fall back to an
|
||||
// empty-data powered result so the template still renders.
|
||||
payload, _ = json.Marshal(PoweredResult{Template: template})
|
||||
// empty-data result while preserving response options.
|
||||
result.Data = nil
|
||||
payload, _ = json.Marshal(result)
|
||||
}
|
||||
return poweredBlockSentinel + string(payload)
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package blocks
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
)
|
||||
|
||||
func TestPoweredBlockRoundTrip(t *testing.T) {
|
||||
marker := PoweredBlock("<p>{{ x }}</p>", map[string]any{"x": "y"})
|
||||
@ -16,6 +20,47 @@ func TestPoweredBlockRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoweredBlockWithOptionsRoundTrip(t *testing.T) {
|
||||
metadata := &document.Metadata{
|
||||
Title: "Missing article",
|
||||
Description: "The requested article was not found.",
|
||||
CanonicalURL: "https://example.com/docs/missing",
|
||||
ImageURL: "https://example.com/media/missing.png",
|
||||
Robots: "noindex,nofollow",
|
||||
}
|
||||
marker := PoweredBlockWithOptions("<h1>{{ title }}</h1>", map[string]any{"title": "Missing"}, PoweredOptions{
|
||||
StatusCode: 404,
|
||||
Metadata: metadata,
|
||||
})
|
||||
got, ok := DecodePoweredBlock(marker)
|
||||
if !ok {
|
||||
t.Fatal("DecodePoweredBlock did not recognize its own marker")
|
||||
}
|
||||
if got.StatusCode != 404 {
|
||||
t.Errorf("status code = %d, want 404", got.StatusCode)
|
||||
}
|
||||
if got.Metadata == nil || *got.Metadata != *metadata {
|
||||
t.Errorf("metadata = %+v, want %+v", got.Metadata, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoweredBlockWithOptionsPreservesOptionsWhenDataCannotMarshal(t *testing.T) {
|
||||
marker := PoweredBlockWithOptions("<h1>fallback</h1>", map[string]any{"bad": make(chan int)}, PoweredOptions{
|
||||
StatusCode: 503,
|
||||
Metadata: &document.Metadata{Title: "Unavailable"},
|
||||
})
|
||||
got, ok := DecodePoweredBlock(marker)
|
||||
if !ok {
|
||||
t.Fatal("DecodePoweredBlock did not recognize fallback marker")
|
||||
}
|
||||
if got.Data != nil {
|
||||
t.Errorf("data = %#v, want nil fallback", got.Data)
|
||||
}
|
||||
if got.StatusCode != 503 || got.Metadata == nil || got.Metadata.Title != "Unavailable" {
|
||||
t.Errorf("options were not preserved: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodePoweredBlockRejectsPlainHTML(t *testing.T) {
|
||||
if _, ok := DecodePoweredBlock("<p>hello</p>"); ok {
|
||||
t.Error("plain HTML must not decode as a powered block")
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
# Provisioned pages use stable keys and render results carry safe response data
|
||||
|
||||
Provisioned pages were identified only by slug. Slugs are customer-facing
|
||||
content and can change when an administrator renames a page, changes its
|
||||
hierarchy, or mounts a plugin below a customer-specific route. A later plugin
|
||||
load could therefore fail to find its page, recreate it, or attach children
|
||||
and menu items to a stale location.
|
||||
|
||||
Decision: a page declaration may carry a plugin-scoped `Key`. `ParentKey`
|
||||
references another declaration from the same plugin, while menu items use the
|
||||
pair `PagePlugin` and `PageKey` so they can target a provisioned page across
|
||||
plugin boundaries. The existing `ParentSlug` field remains an explicit legacy
|
||||
input, but keyed references are the stable identity for new declarations.
|
||||
|
||||
`MountPath` is create-only. The host applies it while creating the page but
|
||||
does not relocate an existing customer page during later reconciliation.
|
||||
`MergeMissingBlockContent` enables a shallow default merge for matching
|
||||
plugin-owned block assignments: only absent top-level keys are added, and
|
||||
present values such as an empty string, zero, false, or null are preserved.
|
||||
|
||||
The render ABI exposes both `DerivedSlug` and `MountPath` on page and block
|
||||
contexts. `DerivedSlug` is computed from the resolved hierarchy. `MountPath`
|
||||
is the effective, normalized value after host inheritance, rather than the raw
|
||||
stored declaration. Guests consume these values and do not reimplement host
|
||||
routing rules.
|
||||
|
||||
Powered blocks and template documents may also request an HTTP status and a
|
||||
shared `DocumentMetadata` value. Metadata is deliberately limited to title,
|
||||
description, canonical URL, image URL, and robots directives. The host
|
||||
validates status codes and metadata, escapes values, and accumulates status
|
||||
before writing headers. Only 200–599 is accepted; the highest status class
|
||||
wins, with the first result retained within a class. For metadata, the first
|
||||
non-empty value for each field wins in deterministic render order, after which
|
||||
page/document defaults fill gaps. Arbitrary meta/link maps were rejected
|
||||
because they would create a second unvalidated head-injection path; advanced
|
||||
trusted templates already have the explicit `HeadExtra` surface.
|
||||
|
||||
Appending fields to the v1 protobuf messages was chosen over renumbering or a
|
||||
Go-only helper because `abi/proto/v1` is the language-neutral contract. Slug
|
||||
fallbacks remain data in that contract; the SDK does not silently synthesize
|
||||
keys from mutable slugs.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Plugin updates can find the same provisioned page after customer route
|
||||
changes without taking ownership of the customer's chosen slug.
|
||||
- Parent and menu references survive page moves and renames.
|
||||
- Changing a declaration's mount path does not unexpectedly move a live page.
|
||||
- Plugins can add newly introduced block defaults without overwriting customer
|
||||
configuration.
|
||||
- Native and WASM render code receive the same host-resolved routing values.
|
||||
- Wiki-like powered blocks can return correct not-found status and structured
|
||||
metadata without rendering or injecting the document envelope themselves.
|
||||
|
||||
Keywords: PageConfig.Key, ParentKey, ParentSlug, MountPath,
|
||||
MergeMissingBlockContent, MenuItemConfig.PagePlugin, PageKey, DerivedSlug,
|
||||
PageSeed, RenderContext, PoweredBlockWithOptions, DocumentMetadata,
|
||||
TemplateDocument, stable identity, page provisioning
|
||||
@ -0,0 +1,37 @@
|
||||
# Plugin manifests declare admin API compatibility
|
||||
|
||||
Plugin admin bundles consume the versioned `@block-ninja/api` browser package.
|
||||
The existing `block_core` compatibility constraint describes the host plugin
|
||||
runtime, but it cannot tell a registry or CMS whether an admin bundle is safe
|
||||
to load against the host's browser API. Treating those surfaces as one version
|
||||
would couple independent release cycles and allow an otherwise compatible
|
||||
plugin to fail only after its admin page loads.
|
||||
|
||||
Decision: `[compatibility]` gains an optional `admin_api` string. Its value is
|
||||
a semantic-version constraint for the host-provided browser admin API, for
|
||||
example `admin_api = ">=0.1.2"`. The manifest/parser layer preserves the value
|
||||
verbatim; registry and host resolvers own constraint validation and matching.
|
||||
|
||||
An omitted or empty value means the plugin has not declared browser API
|
||||
compatibility. It is not equivalent to an unconstrained wildcard. Automated
|
||||
latest-compatible resolution that filters by a host admin API version must
|
||||
therefore fail closed for an undeclared value. This does not change the
|
||||
separate `block_core` constraint or imply that every plugin has an admin
|
||||
bundle.
|
||||
|
||||
The field is part of the shared manifest model and the CLI's hand-written
|
||||
`plugin.mod` serializer so version bumps, tag edits, and initialization cannot
|
||||
silently discard it.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Registries and hosts can select plugin releases compatible with both the
|
||||
runtime ABI and browser admin API.
|
||||
- Headless plugins can continue to omit `admin_api`.
|
||||
- Plugin authors express a range rather than pinning a single browser package
|
||||
version.
|
||||
- Constraint syntax and matching remain the resolver's responsibility rather
|
||||
than being duplicated in manifest parsers.
|
||||
|
||||
Keywords: plugin.mod, compatibility, admin_api, AdminAPI, @block-ninja/api,
|
||||
semantic version, browser API, fail closed, plugin resolver
|
||||
27
docs/adr/0005-plugin-manifests-declare-sdk-compatibility.md
Normal file
27
docs/adr/0005-plugin-manifests-declare-sdk-compatibility.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Plugin manifests declare Plugin SDK compatibility
|
||||
|
||||
The historical `block_core` compatibility field predates the standalone
|
||||
Plugin SDK and is ambiguous: CMS core and the plugin-facing SDK have separate
|
||||
release cycles. Interpreting the same constraint against both versions can
|
||||
select an artifact that the host cannot load or reject one that is compatible.
|
||||
|
||||
Decision: `[compatibility]` gains an optional `plugin_sdk` string containing a
|
||||
semantic-version constraint for the plugin-facing SDK/ABI, for example
|
||||
`plugin_sdk = ">=0.3.7"`. Manifest parsing preserves the value verbatim;
|
||||
registry and host resolvers own constraint validation and matching.
|
||||
|
||||
An omitted or empty value is undeclared compatibility, not a wildcard.
|
||||
Automated resolution against a host SDK version must fail closed, except for
|
||||
an explicitly bounded migration policy for releases published before this
|
||||
field existed. `block_core` remains a separate historical constraint and is
|
||||
not reinterpreted as the CMS application's own version.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Plugins can state the actual SDK/ABI range they require.
|
||||
- CMS core can evolve independently of the public Plugin SDK.
|
||||
- New releases must declare `plugin_sdk` to participate in automatic
|
||||
SDK-compatible selection.
|
||||
|
||||
Keywords: plugin.mod, compatibility, plugin_sdk, PluginSDK, ABI, semantic
|
||||
version, plugin resolver, fail closed
|
||||
@ -0,0 +1,27 @@
|
||||
# Bridge providers receive authenticated caller identity
|
||||
|
||||
Bridge payloads are guest-controlled. A provider that uses a payload field as
|
||||
an ownership namespace lets one plugin impersonate another plugin and mutate
|
||||
or claim its managed records. The consumer-to-host `BridgeInvokeRequest`
|
||||
cannot safely carry identity because the consumer constructs that message.
|
||||
|
||||
Decision: the host derives the caller plugin from the loaded module or native
|
||||
plugin registration. It passes that identity to the provider in
|
||||
`BridgeCallRequest.caller_plugin` and, for Go providers, through
|
||||
`plugin.WithBridgeCallerPlugin`. Providers read it with
|
||||
`plugin.BridgeCallerPlugin` and fail closed when an ownership-sensitive call
|
||||
has no authenticated caller.
|
||||
|
||||
The new host-to-provider protobuf field is additive. Older providers ignore
|
||||
it. New ownership-sensitive providers intentionally reject calls from older
|
||||
hosts that cannot authenticate a caller. Hosts must never copy a caller name
|
||||
from opaque bridge payloads or a consumer-authored capability field.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Providers can derive durable ownership from an authenticated principal.
|
||||
- Bridge payload schemas do not need security-sensitive source fields.
|
||||
- Native and Wasm providers observe the same caller context contract.
|
||||
|
||||
Keywords: plugin bridge, caller identity, authentication, ownership,
|
||||
BridgeCallRequest, WithBridgeCallerPlugin, Wasm, confused deputy
|
||||
@ -0,0 +1,36 @@
|
||||
# Register sees the current guest host services
|
||||
|
||||
Go guest plugins can declare capability-backed runtime surfaces from their
|
||||
`Register` callback. Bridge providers are the first such surface: the provider
|
||||
stores its invokable value in the guest-local bridge stub while the stub tells
|
||||
the CMS host that the named service exists.
|
||||
|
||||
`Serve` previously assigned the package-level guest runtime only after
|
||||
`newGuest` returned. Because `newGuest` runs `Register` synchronously,
|
||||
`HostServices()` returned an empty `CoreServices` value during registration.
|
||||
Plugins that correctly nil-checked the bridge silently skipped registration.
|
||||
The CMS could therefore mark a Wasm plugin loaded while consumers failed with
|
||||
`bridge: no service registered` during a later load hook.
|
||||
|
||||
`newGuest` now makes the guest under construction current for the synchronous
|
||||
registration pass and restores the previous runtime before returning. `Serve`
|
||||
then installs the completed guest as before. This keeps construction isolated
|
||||
for native tests while making the documented `HostServices()` escape hatch
|
||||
truthful during `Register` on every pooled Wasm instance.
|
||||
|
||||
Alternatives rejected were moving bridge registration into `Load`, which runs
|
||||
on only one pooled instance, and adding bridge names only to the manifest,
|
||||
which would advertise host availability without installing the guest-local
|
||||
`BridgeInvokable` value needed by `HOOK_BRIDGE_CALL`.
|
||||
|
||||
Consequences:
|
||||
|
||||
- Bridge services registered from `Register` exist on every pooled guest
|
||||
instance and remain callable after CMS startup and hot swap.
|
||||
- A regression test exercises the observable `Serve` plus
|
||||
`HOOK_BRIDGE_CALL` contract instead of relying on initialization internals.
|
||||
- No ABI or protobuf change is required; consumers need a plugin SDK release
|
||||
containing the corrected Go guest runtime.
|
||||
|
||||
Keywords: wasmguest.Serve, newGuest, HostServices, PluginRegistration.Register,
|
||||
plugin bridge, RegisterService, BridgeInvokable, HOOK_BRIDGE_CALL, wiki content
|
||||
14
document/metadata.go
Normal file
14
document/metadata.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Package document defines structured page-response metadata shared by block
|
||||
// and template render results.
|
||||
package document
|
||||
|
||||
// Metadata is the safe document-head subset a plugin may ask the host to
|
||||
// render. The host remains responsible for validating URLs and robots values
|
||||
// and for escaping every value before it is written into HTML.
|
||||
type Metadata struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CanonicalURL string `json:"canonicalUrl,omitempty"`
|
||||
ImageURL string `json:"imageUrl,omitempty"`
|
||||
Robots string `json:"robots,omitempty"`
|
||||
}
|
||||
@ -166,12 +166,12 @@ func validHostname(h string) bool {
|
||||
if len(h) > 253 {
|
||||
return false
|
||||
}
|
||||
for _, label := range strings.Split(h, ".") {
|
||||
for label := range strings.SplitSeq(h, ".") {
|
||||
if label == "" || len(label) > 63 {
|
||||
return false
|
||||
}
|
||||
for _, r := range label {
|
||||
if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '-' {
|
||||
if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,26 @@ package plugin
|
||||
|
||||
import "context"
|
||||
|
||||
type bridgeCallerPluginContextKey struct{}
|
||||
|
||||
// WithBridgeCallerPlugin records the host-authenticated plugin name of a
|
||||
// bridge caller. Hosts and ABI adapters must derive pluginName from the loaded
|
||||
// caller, never from guest-controlled request data.
|
||||
func WithBridgeCallerPlugin(ctx context.Context, pluginName string) context.Context {
|
||||
return context.WithValue(ctx, bridgeCallerPluginContextKey{}, pluginName)
|
||||
}
|
||||
|
||||
// BridgeCallerPlugin returns the host-authenticated plugin name of the
|
||||
// current bridge caller. Ownership-sensitive providers should fail closed
|
||||
// when no caller identity is present.
|
||||
func BridgeCallerPlugin(ctx context.Context) (string, bool) {
|
||||
if ctx == nil {
|
||||
return "", false
|
||||
}
|
||||
pluginName, ok := ctx.Value(bridgeCallerPluginContextKey{}).(string)
|
||||
return pluginName, ok && pluginName != ""
|
||||
}
|
||||
|
||||
// PluginBridge allows plugins to share services with each other.
|
||||
// Plugins register named services during startup; other plugins look them up at runtime.
|
||||
//
|
||||
|
||||
25
plugin/bridge_test.go
Normal file
25
plugin/bridge_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBridgeCallerPlugin(t *testing.T) {
|
||||
if got, ok := BridgeCallerPlugin(context.Background()); ok || got != "" {
|
||||
t.Fatalf("BridgeCallerPlugin(background) = %q, %t, want empty, false", got, ok)
|
||||
}
|
||||
|
||||
ctx := WithBridgeCallerPlugin(context.Background(), "website")
|
||||
if got, ok := BridgeCallerPlugin(ctx); !ok || got != "website" {
|
||||
t.Fatalf("BridgeCallerPlugin(ctx) = %q, %t, want website, true", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithBridgeCallerPluginEmptyDoesNotAuthenticateCaller(t *testing.T) {
|
||||
ctx := WithBridgeCallerPlugin(context.Background(), "spoofed")
|
||||
ctx = WithBridgeCallerPlugin(ctx, "")
|
||||
if got, ok := BridgeCallerPlugin(ctx); ok || got != "" {
|
||||
t.Fatalf("BridgeCallerPlugin(ctx) = %q, %t, want empty, false", got, ok)
|
||||
}
|
||||
}
|
||||
@ -91,6 +91,8 @@ type ModPublicRoute struct {
|
||||
|
||||
type ModCompat struct {
|
||||
BlockCore string `toml:"block_core"`
|
||||
AdminAPI string `toml:"admin_api"`
|
||||
PluginSDK string `toml:"plugin_sdk"`
|
||||
}
|
||||
|
||||
type ModRequirement struct {
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tomlpkg "github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
func TestParseModFull_BasicFields(t *testing.T) {
|
||||
@ -278,6 +281,8 @@ version = "0.2.0"
|
||||
|
||||
[compatibility]
|
||||
block_core = ">=1.5 <2.0"
|
||||
admin_api = ">=0.1.2 <0.2.0"
|
||||
plugin_sdk = ">=0.3.7 <0.4.0"
|
||||
|
||||
[[requires]]
|
||||
name = "@blockninja/smartblock"
|
||||
@ -291,8 +296,17 @@ version = ">=1.2"
|
||||
if err != nil {
|
||||
t.Fatalf("ParseModFull err: %v", err)
|
||||
}
|
||||
if m.Compatibility == nil || m.Compatibility.BlockCore != ">=1.5 <2.0" {
|
||||
t.Errorf("Compat = %+v", m.Compatibility)
|
||||
if m.Compatibility == nil {
|
||||
t.Fatal("Compatibility is nil")
|
||||
}
|
||||
if m.Compatibility.BlockCore != ">=1.5 <2.0" {
|
||||
t.Errorf("Compat.BlockCore = %q", m.Compatibility.BlockCore)
|
||||
}
|
||||
if m.Compatibility.AdminAPI != ">=0.1.2 <0.2.0" {
|
||||
t.Errorf("Compat.AdminAPI = %q", m.Compatibility.AdminAPI)
|
||||
}
|
||||
if m.Compatibility.PluginSDK != ">=0.3.7 <0.4.0" {
|
||||
t.Errorf("Compat.PluginSDK = %q", m.Compatibility.PluginSDK)
|
||||
}
|
||||
if len(m.Requires) != 2 {
|
||||
t.Fatalf("Requires len = %d, want 2", len(m.Requires))
|
||||
@ -305,6 +319,33 @@ version = ">=1.2"
|
||||
}
|
||||
}
|
||||
|
||||
func TestModCompatPluginSDKRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := &ModFile{
|
||||
Plugin: ModPlugin{Name: "wiki", Version: "0.2.0"},
|
||||
Compatibility: &ModCompat{
|
||||
BlockCore: ">=0.3.4",
|
||||
AdminAPI: ">=0.1.2",
|
||||
PluginSDK: ">=0.3.7 <0.4.0",
|
||||
},
|
||||
}
|
||||
var encoded bytes.Buffer
|
||||
if err := tomlpkg.NewEncoder(&encoded).Encode(want); err != nil {
|
||||
t.Fatalf("encode plugin.mod: %v", err)
|
||||
}
|
||||
if !strings.Contains(encoded.String(), `plugin_sdk = ">=0.3.7 <0.4.0"`) {
|
||||
t.Fatalf("encoded plugin.mod omitted plugin_sdk:\n%s", encoded.String())
|
||||
}
|
||||
got, err := ParseModFull(encoded.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseModFull(round trip): %v", err)
|
||||
}
|
||||
if got.Compatibility == nil || got.Compatibility.PluginSDK != want.Compatibility.PluginSDK {
|
||||
t.Fatalf("round-trip PluginSDK = %#v, want %q", got.Compatibility, want.Compatibility.PluginSDK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeTags_HappyPath(t *testing.T) {
|
||||
got, err := NormalizeTags([]string{"dark", "agency", "serif"})
|
||||
if err != nil {
|
||||
|
||||
@ -41,8 +41,24 @@ type DataTableConfig struct {
|
||||
|
||||
// PageConfig defines a page to provision.
|
||||
type PageConfig struct {
|
||||
// Key is the stable, plugin-scoped identity used to find the page across
|
||||
// slug and hierarchy changes.
|
||||
Key string
|
||||
|
||||
Slug string
|
||||
|
||||
// ParentKey identifies a parent provisioned by the same plugin.
|
||||
ParentKey string
|
||||
|
||||
// ParentSlug is the legacy slug-based parent reference. New declarations
|
||||
// should use ParentKey so moving or renaming the parent does not break the
|
||||
// relationship.
|
||||
ParentSlug string
|
||||
|
||||
// MountPath is applied when the page is first created. Reconciliation does
|
||||
// not move an existing page when this value later changes.
|
||||
MountPath string
|
||||
|
||||
Title string
|
||||
TemplateKey string
|
||||
Blocks []PageBlockConfig
|
||||
@ -51,6 +67,10 @@ type PageConfig struct {
|
||||
DetailSlugField string
|
||||
ReconcileBlocks bool
|
||||
ReconcileTemplate bool
|
||||
// MergeMissingBlockContent shallowly adds absent top-level default keys to
|
||||
// matching plugin-owned block assignments. Present values, including empty
|
||||
// strings, zero, false, and null, are preserved.
|
||||
MergeMissingBlockContent bool
|
||||
}
|
||||
|
||||
// PageBlockConfig defines a block within a provisioned page.
|
||||
@ -75,6 +95,11 @@ type MenuItemConfig struct {
|
||||
Label string
|
||||
URL string
|
||||
PageSlug string
|
||||
// PagePlugin and PageKey identify a provisioned page independently of its
|
||||
// current slug. PagePlugin permits a menu declared by one plugin to target
|
||||
// a page owned by another.
|
||||
PagePlugin string
|
||||
PageKey string
|
||||
SortOrder int32
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ func checkRoutePath(r ModPublicRoute) string {
|
||||
if strings.HasSuffix(p, "/") {
|
||||
return "no trailing slash; set prefix = true to claim the subtree"
|
||||
}
|
||||
for _, seg := range strings.Split(p[1:], "/") {
|
||||
for seg := range strings.SplitSeq(p[1:], "/") {
|
||||
switch {
|
||||
case seg == "":
|
||||
return "empty path segment"
|
||||
|
||||
@ -790,7 +790,9 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
||||
resp: &abiv1.ProvisionerEnsurePageResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
err := cs.Provisioner.EnsurePage(plugin.PageConfig{
|
||||
Slug: "/", Title: "Home", TemplateKey: "homepage", ReconcileBlocks: true,
|
||||
Key: "home", Slug: "/", ParentKey: "site", ParentSlug: "/legacy-site",
|
||||
MountPath: "/knowledge", Title: "Home", TemplateKey: "homepage",
|
||||
ReconcileBlocks: true, MergeMissingBlockContent: true,
|
||||
Blocks: []plugin.PageBlockConfig{
|
||||
{BlockKey: "html", Title: "Hero", Content: map[string]any{"x": float64(1)}, Slot: "main", SortOrder: 1},
|
||||
},
|
||||
@ -813,7 +815,9 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
||||
name: "provisioner_ensure_menu_item", wantMethod: "provisioner.ensure_menu_item",
|
||||
resp: &abiv1.ProvisionerEnsureMenuItemResponse{},
|
||||
run: func(t *testing.T, cs plugin.CoreServices) {
|
||||
err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{Label: "Docs", PageSlug: "/docs", SortOrder: 3})
|
||||
err := cs.Provisioner.EnsureMenuItem("main", plugin.MenuItemConfig{
|
||||
Label: "Docs", PageSlug: "/docs", PagePlugin: "wiki", PageKey: "index", SortOrder: 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1008,6 +1012,41 @@ func TestCapabilityRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPageContractGolden(t *testing.T) {
|
||||
raw, err := os.ReadFile(goldenPath("provisioner_ensure_page_req"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := &abiv1.ProvisionerEnsurePageRequest{}
|
||||
if err := proto.Unmarshal(raw, req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
page := req.GetPage()
|
||||
if page == nil {
|
||||
t.Fatal("page is nil")
|
||||
}
|
||||
if page.GetKey() != "home" || page.GetSlug() != "/" ||
|
||||
page.GetParentKey() != "site" || page.GetParentSlug() != "/legacy-site" ||
|
||||
page.GetMountPath() != "/knowledge" || !page.GetMergeMissingBlockContent() {
|
||||
t.Errorf("page seed = %+v", page)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerMenuPageReferenceContractGolden(t *testing.T) {
|
||||
raw, err := os.ReadFile(goldenPath("provisioner_ensure_menu_item_req"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := &abiv1.ProvisionerEnsureMenuItemRequest{}
|
||||
if err := proto.Unmarshal(raw, req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if req.GetMenuName() != "main" || req.GetPageSlug() != "/docs" ||
|
||||
req.GetPagePlugin() != "wiki" || req.GetPageKey() != "index" {
|
||||
t.Errorf("menu request = %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeAbiErr is a transport error carrying an ABI code, like wasmguest.HostError.
|
||||
type fakeAbiErr struct {
|
||||
code abiv1.AbiErrorCode
|
||||
|
||||
@ -53,8 +53,11 @@ func (s *provisionerStub) EnsureSetting(key string, defaultValue any) error {
|
||||
|
||||
func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error {
|
||||
seed := &abiv1.PageSeed{
|
||||
Key: config.Key,
|
||||
Slug: config.Slug,
|
||||
ParentKey: config.ParentKey,
|
||||
ParentSlug: config.ParentSlug,
|
||||
MountPath: config.MountPath,
|
||||
Title: config.Title,
|
||||
TemplateKey: config.TemplateKey,
|
||||
DetailSourceType: config.DetailSourceType,
|
||||
@ -62,6 +65,7 @@ func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error {
|
||||
DetailSlugField: config.DetailSlugField,
|
||||
ReconcileBlocks: config.ReconcileBlocks,
|
||||
ReconcileTemplate: config.ReconcileTemplate,
|
||||
MergeMissingBlockContent: config.MergeMissingBlockContent,
|
||||
}
|
||||
for _, b := range config.Blocks {
|
||||
contentJSON, err := json.Marshal(b.Content)
|
||||
@ -96,6 +100,8 @@ func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItem
|
||||
Label: config.Label,
|
||||
Url: config.URL,
|
||||
PageSlug: config.PageSlug,
|
||||
PagePlugin: config.PagePlugin,
|
||||
PageKey: config.PageKey,
|
||||
SortOrder: config.SortOrder,
|
||||
}
|
||||
return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{})
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
|
||||
mainDocs"/docs(
|
||||
mainDocs"/docs(2wiki:index
|
||||
@ -1,4 +1,5 @@
|
||||
|
||||
4
|
||||
/Home"homepage*
|
||||
htmlHero{"x":1}*main0H
|
||||
\
|
||||
//legacy-siteHome"homepage*
|
||||
htmlHero{"x":1}*main0HZhomebsitej
|
||||
/knowledgep
|
||||
@ -39,6 +39,8 @@ func contextFromRenderContext(ctx context.Context, rc *abiv1.RenderContext) cont
|
||||
ctx = blocks.WithCurrentPage(ctx, &blocks.PageContext{
|
||||
ID: parseUUID(p.GetId()),
|
||||
Slug: p.GetSlug(),
|
||||
DerivedSlug: p.GetDerivedSlug(),
|
||||
MountPath: p.GetMountPath(),
|
||||
Title: p.GetTitle(),
|
||||
PostType: p.GetPostType(),
|
||||
Status: p.GetStatus(),
|
||||
@ -159,6 +161,8 @@ func blockContextFromProto(pb *abiv1.BlockContext) *blocks.BlockContext {
|
||||
URL: pb.GetUrl(),
|
||||
Path: pb.GetPath(),
|
||||
Slug: pb.GetSlug(),
|
||||
DerivedSlug: pb.GetDerivedSlug(),
|
||||
MountPath: pb.GetMountPath(),
|
||||
PageID: pb.GetPageId(),
|
||||
PageTitle: pb.GetPageTitle(),
|
||||
TemplateKey: pb.GetTemplateKey(),
|
||||
|
||||
51
plugin/wasmguest/context_test.go
Normal file
51
plugin/wasmguest/context_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
package wasmguest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestContextFromRenderContextCarriesResolvedPagePaths(t *testing.T) {
|
||||
pageID := uuid.MustParse("5f0c5f3f-3f1e-4a3a-9b6e-2f4f6a6d7e8f")
|
||||
ctx := contextFromRenderContext(context.Background(), &abiv1.RenderContext{
|
||||
Page: &abiv1.PageContext{
|
||||
Id: pageID.String(),
|
||||
Slug: "/article",
|
||||
DerivedSlug: "/knowledge/article",
|
||||
MountPath: "/knowledge",
|
||||
Title: "Article",
|
||||
PostType: "page",
|
||||
Status: "published",
|
||||
},
|
||||
BlockContext: &abiv1.BlockContext{
|
||||
Slug: "/article",
|
||||
DerivedSlug: "/knowledge/article",
|
||||
MountPath: "/knowledge",
|
||||
},
|
||||
})
|
||||
|
||||
page := blocks.GetCurrentPage(ctx)
|
||||
if page == nil {
|
||||
t.Fatal("current page is nil")
|
||||
}
|
||||
if page.ID != pageID || page.Slug != "/article" ||
|
||||
page.DerivedSlug != "/knowledge/article" || page.MountPath != "/knowledge" {
|
||||
t.Errorf("page context = %+v", page)
|
||||
}
|
||||
|
||||
block := blocks.GetBlockContext(ctx)
|
||||
if block == nil {
|
||||
t.Fatal("block context is nil")
|
||||
}
|
||||
if block.Slug != "/article" || block.DerivedSlug != "/knowledge/article" || block.MountPath != "/knowledge" {
|
||||
t.Errorf("block context = %+v", block)
|
||||
}
|
||||
data := block.ToMap()
|
||||
if data["derivedSlug"] != "/knowledge/article" || data["mountPath"] != "/knowledge" {
|
||||
t.Errorf("block template map = %#v", data)
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import (
|
||||
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/ai"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps"
|
||||
@ -99,7 +100,15 @@ func newGuest(reg plugin.PluginRegistration) *guest {
|
||||
// Pool whose calls fail cleanly, matching the capability stubs.
|
||||
g.services.Pool = bnwasm.NewPool(bnwasm.Transport(capTransport))
|
||||
g.rag, _ = g.services.RAGService.(*caps.RAGStub)
|
||||
// Register callbacks may use HostServices for capability-backed surfaces
|
||||
// that are not passed as Register parameters, such as bridge services.
|
||||
// Publish this partially initialized guest only for the synchronous
|
||||
// registration pass, then restore the previous runtime. Serve installs g
|
||||
// permanently after newGuest returns.
|
||||
previous := current
|
||||
current = g
|
||||
g.registerErr = runRegister(reg, g.templates, g.blocks)
|
||||
current = previous
|
||||
return g
|
||||
}
|
||||
|
||||
@ -253,7 +262,12 @@ func (g *guest) renderBlock(ctx context.Context, payload []byte) (proto.Message,
|
||||
return nil, internalError("marshal powered block data: " + err.Error())
|
||||
}
|
||||
return &abiv1.RenderBlockResponse{
|
||||
Powered: &abiv1.PoweredBlock{Template: pr.Template, DataJson: dataJSON},
|
||||
Powered: &abiv1.PoweredBlock{
|
||||
Template: pr.Template,
|
||||
DataJson: dataJSON,
|
||||
StatusCode: pr.StatusCode,
|
||||
Metadata: documentMetadataToProto(pr.Metadata),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return &abiv1.RenderBlockResponse{Html: out}, nil
|
||||
@ -364,6 +378,8 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
|
||||
tdoc.HtmlAttrs = pd.HTMLAttrs
|
||||
tdoc.HeadExtraHtml = head
|
||||
tdoc.BodyEndExtraHtml = end
|
||||
tdoc.StatusCode = pd.StatusCode
|
||||
tdoc.Metadata = documentMetadataToProto(pd.Metadata)
|
||||
} else {
|
||||
body, err := renderPart(component)
|
||||
if err != nil {
|
||||
@ -374,6 +390,19 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
|
||||
return &abiv1.RenderTemplateResponse{Document: tdoc}, nil
|
||||
}
|
||||
|
||||
func documentMetadataToProto(metadata *document.Metadata) *abiv1.DocumentMetadata {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
return &abiv1.DocumentMetadata{
|
||||
Title: metadata.Title,
|
||||
Description: metadata.Description,
|
||||
CanonicalUrl: metadata.CanonicalURL,
|
||||
ImageUrl: metadata.ImageURL,
|
||||
Robots: metadata.Robots,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) {
|
||||
if g.reg.HTTPHandler == nil {
|
||||
return nil, &abiv1.AbiError{
|
||||
@ -692,6 +721,7 @@ func (g *guest) bridgeCall(ctx context.Context, payload []byte) (proto.Message,
|
||||
Message: "bridge service " + req.GetServiceName() + " does not implement plugin.BridgeInvokable",
|
||||
}
|
||||
}
|
||||
ctx = plugin.WithBridgeCallerPlugin(ctx, req.GetCallerPlugin())
|
||||
out, err := invokable.InvokeBridge(ctx, req.GetMethod(), req.GetPayload())
|
||||
if err != nil {
|
||||
return nil, internalError(err.Error())
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/templates"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@ -24,6 +25,12 @@ func (c textComponent) Render(_ context.Context, w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
type bridgeInvokableFunc func(context.Context, string, []byte) ([]byte, error)
|
||||
|
||||
func (f bridgeInvokableFunc) InvokeBridge(ctx context.Context, method string, payload []byte) ([]byte, error) {
|
||||
return f(ctx, method, payload)
|
||||
}
|
||||
|
||||
func fixtureRegistration() plugin.PluginRegistration {
|
||||
return plugin.PluginRegistration{
|
||||
Name: "fixture",
|
||||
@ -123,6 +130,76 @@ func TestServeInstallsRuntime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeExposesHostServicesDuringRegister(t *testing.T) {
|
||||
previous := current
|
||||
t.Cleanup(func() { current = previous })
|
||||
|
||||
Serve(plugin.PluginRegistration{
|
||||
Name: "bridge-provider",
|
||||
Version: "0.1.0",
|
||||
Register: func(_ templates.TemplateRegistry, _ blocks.BlockRegistry) error {
|
||||
bridge := HostServices().Bridge
|
||||
if bridge == nil {
|
||||
return fmt.Errorf("HostServices bridge is unavailable during Register")
|
||||
}
|
||||
bridge.RegisterService("bridge-provider", "content", bridgeInvokableFunc(
|
||||
func(_ context.Context, _ string, _ []byte) ([]byte, error) {
|
||||
return []byte("registered"), nil
|
||||
},
|
||||
))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
resp := invokeHook(t, current, abiv1.Hook_HOOK_BRIDGE_CALL, &abiv1.BridgeCallRequest{
|
||||
ServiceName: "content",
|
||||
Method: "apply",
|
||||
})
|
||||
if resp.GetError() != nil {
|
||||
t.Fatalf("bridge call returned error: %v", resp.GetError())
|
||||
}
|
||||
bridgeResp := &abiv1.BridgeCallResponse{}
|
||||
if err := proto.Unmarshal(resp.GetPayload(), bridgeResp); err != nil {
|
||||
t.Fatalf("unmarshal BridgeCallResponse: %v", err)
|
||||
}
|
||||
if got := string(bridgeResp.GetPayload()); got != "registered" {
|
||||
t.Fatalf("bridge payload = %q, want registered", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBridgeCallProvidesAuthenticatedCallerContext(t *testing.T) {
|
||||
g := newGuest(fixtureRegistration())
|
||||
g.services.Bridge.RegisterService("fixture", "content", bridgeInvokableFunc(
|
||||
func(ctx context.Context, method string, payload []byte) ([]byte, error) {
|
||||
caller, ok := plugin.BridgeCallerPlugin(ctx)
|
||||
if !ok || caller != "website" {
|
||||
t.Fatalf("BridgeCallerPlugin(ctx) = %q, %t, want website, true", caller, ok)
|
||||
}
|
||||
if method != "ApplyBundle" || string(payload) != `{"revision":"1"}` {
|
||||
t.Fatalf("bridge call = %q %q", method, payload)
|
||||
}
|
||||
return []byte(`{"created":1}`), nil
|
||||
},
|
||||
))
|
||||
|
||||
resp := invokeHook(t, g, abiv1.Hook_HOOK_BRIDGE_CALL, &abiv1.BridgeCallRequest{
|
||||
ServiceName: "content",
|
||||
Method: "ApplyBundle",
|
||||
Payload: []byte(`{"revision":"1"}`),
|
||||
CallerPlugin: "website",
|
||||
})
|
||||
if resp.GetError() != nil {
|
||||
t.Fatalf("bridge call returned error: %v", resp.GetError())
|
||||
}
|
||||
bridgeResp := &abiv1.BridgeCallResponse{}
|
||||
if err := proto.Unmarshal(resp.GetPayload(), bridgeResp); err != nil {
|
||||
t.Fatalf("unmarshal BridgeCallResponse: %v", err)
|
||||
}
|
||||
if got := string(bridgeResp.GetPayload()); got != `{"created":1}` {
|
||||
t.Fatalf("bridge payload = %q, want report", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeManifest(t *testing.T) {
|
||||
g := newGuest(fixtureRegistration())
|
||||
resp := invokeHook(t, g, abiv1.Hook_HOOK_DESCRIBE, &abiv1.DescribeRequest{HostAbiVersion: 1})
|
||||
@ -383,6 +460,14 @@ func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
|
||||
HTMLAttrs: map[string]string{"data-theme": "x"},
|
||||
HeadExtra: textComponent(`<meta name="e">`),
|
||||
BodyEndExtra: textComponent("<script>1</script>"),
|
||||
StatusCode: 201,
|
||||
Metadata: &document.Metadata{
|
||||
Title: "Page title",
|
||||
Description: "Page description",
|
||||
CanonicalURL: "https://example.com/page",
|
||||
ImageURL: "https://example.com/page.png",
|
||||
Robots: "index,follow",
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
@ -419,6 +504,17 @@ func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
|
||||
if string(doc.GetBodyEndExtraHtml()) != "<script>1</script>" {
|
||||
t.Errorf("body_end_extra_html = %q", doc.GetBodyEndExtraHtml())
|
||||
}
|
||||
if doc.GetStatusCode() != 201 {
|
||||
t.Errorf("status_code = %d, want 201", doc.GetStatusCode())
|
||||
}
|
||||
metadata := doc.GetMetadata()
|
||||
if metadata == nil || metadata.GetTitle() != "Page title" ||
|
||||
metadata.GetDescription() != "Page description" ||
|
||||
metadata.GetCanonicalUrl() != "https://example.com/page" ||
|
||||
metadata.GetImageUrl() != "https://example.com/page.png" ||
|
||||
metadata.GetRobots() != "index,follow" {
|
||||
t.Errorf("metadata = %+v", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobHook(t *testing.T) {
|
||||
|
||||
@ -74,8 +74,8 @@ func methodDocumentation(method protoreflect.MethodDescriptor) (description, doc
|
||||
}
|
||||
paragraph = strings.TrimSpace(strings.ReplaceAll(paragraph, "\n", " "))
|
||||
methodName := string(method.Name())
|
||||
if strings.HasPrefix(paragraph, methodName+" ") {
|
||||
paragraph = strings.TrimSpace(strings.TrimPrefix(paragraph, methodName+" "))
|
||||
if after, ok := strings.CutPrefix(paragraph, methodName+" "); ok {
|
||||
paragraph = strings.TrimSpace(after)
|
||||
if paragraph != "" {
|
||||
runes := []rune(paragraph)
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
|
||||
@ -65,30 +65,30 @@ func registerMCPFixture(t *testing.T) {
|
||||
return
|
||||
}
|
||||
file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
|
||||
Name: proto.String("test/mcpfixture.proto"),
|
||||
Package: proto.String("mcpfixture.v1"),
|
||||
Syntax: proto.String("proto3"),
|
||||
Name: new("test/mcpfixture.proto"),
|
||||
Package: new("mcpfixture.v1"),
|
||||
Syntax: new("proto3"),
|
||||
Dependency: []string{"google/protobuf/timestamp.proto"},
|
||||
MessageType: []*descriptorpb.DescriptorProto{{
|
||||
Name: proto.String("ListArticlesRequest"),
|
||||
Name: new("ListArticlesRequest"),
|
||||
Field: []*descriptorpb.FieldDescriptorProto{
|
||||
{Name: proto.String("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
|
||||
{Name: proto.String("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
|
||||
{Name: proto.String("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".google.protobuf.Timestamp")},
|
||||
{Name: new("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
|
||||
{Name: new("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
|
||||
{Name: new("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: new(".google.protobuf.Timestamp")},
|
||||
},
|
||||
}, {
|
||||
Name: proto.String("ListArticlesResponse"),
|
||||
Name: new("ListArticlesResponse"),
|
||||
}},
|
||||
Service: []*descriptorpb.ServiceDescriptorProto{{
|
||||
Name: proto.String("WikiService"),
|
||||
Name: new("WikiService"),
|
||||
Method: []*descriptorpb.MethodDescriptorProto{{
|
||||
Name: proto.String("ListArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"),
|
||||
Name: new("ListArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"),
|
||||
}, {
|
||||
Name: proto.String("StreamArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: proto.Bool(true),
|
||||
Name: new("StreamArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: new(true),
|
||||
}},
|
||||
}},
|
||||
SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{
|
||||
Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: proto.String("ListArticles lists Wiki articles.\n\nSupports pagination."),
|
||||
Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: new("ListArticles lists Wiki articles.\n\nSupports pagination."),
|
||||
}}},
|
||||
}, protoregistry.GlobalFiles)
|
||||
if err != nil {
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
|
||||
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/templates"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@ -25,7 +26,20 @@ func tagFilterRegistration() plugin.PluginRegistration {
|
||||
func(ctx context.Context, content map[string]any) string {
|
||||
title, _ := content["title"].(string)
|
||||
// Powered: return template + data; the host renders it.
|
||||
return blocks.PoweredBlock("<h1>{{ title }}</h1>", map[string]any{"title": title})
|
||||
return blocks.PoweredBlockWithOptions(
|
||||
"<h1>{{ title }}</h1>",
|
||||
map[string]any{"title": title},
|
||||
blocks.PoweredOptions{
|
||||
StatusCode: 404,
|
||||
Metadata: &document.Metadata{
|
||||
Title: "Missing",
|
||||
Description: "Missing article",
|
||||
CanonicalURL: "https://example.com/missing",
|
||||
ImageURL: "https://example.com/missing.png",
|
||||
Robots: "noindex,nofollow",
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
blocks.RegisterTag("greet", func(args map[string]any, rctx blocks.RenderContext) (string, error) {
|
||||
who, _ := args["who"].(string)
|
||||
@ -111,6 +125,17 @@ func TestPoweredBlockRoundTrip(t *testing.T) {
|
||||
if !strings.Contains(string(p.GetDataJson()), `"title":"Welcome"`) {
|
||||
t.Errorf("data_json = %s", p.GetDataJson())
|
||||
}
|
||||
if p.GetStatusCode() != 404 {
|
||||
t.Errorf("status_code = %d, want 404", p.GetStatusCode())
|
||||
}
|
||||
metadata := p.GetMetadata()
|
||||
if metadata == nil || metadata.GetTitle() != "Missing" ||
|
||||
metadata.GetDescription() != "Missing article" ||
|
||||
metadata.GetCanonicalUrl() != "https://example.com/missing" ||
|
||||
metadata.GetImageUrl() != "https://example.com/missing.png" ||
|
||||
metadata.GetRobots() != "noindex,nofollow" {
|
||||
t.Errorf("metadata = %+v", metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTagRoundTrip(t *testing.T) {
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/templates"
|
||||
)
|
||||
@ -45,7 +46,20 @@ var Registration = plugin.PluginRegistration{
|
||||
if powered, _ := content["powered"].(bool); powered {
|
||||
// Powered path: hand the host a template + data instead of final
|
||||
// HTML, so the host renders it (pongo2) after this call returns.
|
||||
return blocks.PoweredBlock("<p>hello {{ name }}</p>", map[string]any{"name": name})
|
||||
return blocks.PoweredBlockWithOptions(
|
||||
"<p>hello {{ name }}</p>",
|
||||
map[string]any{"name": name},
|
||||
blocks.PoweredOptions{
|
||||
StatusCode: 404,
|
||||
Metadata: &document.Metadata{
|
||||
Title: "Missing greeting",
|
||||
Description: "The greeting was not found.",
|
||||
CanonicalURL: "https://example.com/greetings/missing",
|
||||
ImageURL: "https://example.com/greetings/missing.png",
|
||||
Robots: "noindex,nofollow",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf("<p>hello %s</p>", name)
|
||||
})
|
||||
|
||||
@ -241,6 +241,17 @@ func TestWasmFixtureTagFilterPoweredRoundTrip(t *testing.T) {
|
||||
if !strings.Contains(string(p.GetDataJson()), `"name":"wazero"`) {
|
||||
t.Errorf("powered data_json = %s", p.GetDataJson())
|
||||
}
|
||||
if p.GetStatusCode() != 404 {
|
||||
t.Errorf("powered status_code = %d, want 404", p.GetStatusCode())
|
||||
}
|
||||
metadata := p.GetMetadata()
|
||||
if metadata == nil || metadata.GetTitle() != "Missing greeting" ||
|
||||
metadata.GetDescription() != "The greeting was not found." ||
|
||||
metadata.GetCanonicalUrl() != "https://example.com/greetings/missing" ||
|
||||
metadata.GetImageUrl() != "https://example.com/greetings/missing.png" ||
|
||||
metadata.GetRobots() != "noindex,nofollow" {
|
||||
t.Errorf("powered metadata = %+v", metadata)
|
||||
}
|
||||
|
||||
// HOOK_RENDER_TAG dispatches to the registered tag fn (fresh invoke —
|
||||
// no re-entrancy; RENDER_BLOCK already returned).
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/blocks"
|
||||
@ -350,7 +351,7 @@ func renderBlock(ctx context.Context, block map[string]any) string {
|
||||
}
|
||||
|
||||
img := fmt.Sprintf(`<img src="%s" alt="%s" />`, html.EscapeString(url), html.EscapeString(alt))
|
||||
if link != "" {
|
||||
if link = safeLinkURL(link); link != "" {
|
||||
img = fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(link), img)
|
||||
}
|
||||
// The positioning wrapper only exists when a chip is rendered, so
|
||||
@ -424,8 +425,8 @@ func renderBlock(ctx context.Context, block map[string]any) string {
|
||||
name = url
|
||||
}
|
||||
sb.WriteString(`<div class="my-4 rounded border border-border p-4">`)
|
||||
if url != "" {
|
||||
fmt.Fprintf(&sb, `<a class="text-primary underline" href="%s">`, html.EscapeString(url))
|
||||
if href := safeLinkURL(url); href != "" {
|
||||
fmt.Fprintf(&sb, `<a class="text-primary underline" href="%s">`, html.EscapeString(href))
|
||||
sb.WriteString(html.EscapeString(name))
|
||||
sb.WriteString("</a>")
|
||||
} else {
|
||||
@ -627,6 +628,7 @@ func renderInlineContent(content []map[string]any, insideLink bool) string {
|
||||
|
||||
case "link":
|
||||
href, _ := itemMap["href"].(string)
|
||||
href = safeLinkURL(href)
|
||||
linkContent := inlineContentFromRaw(itemMap["content"])
|
||||
if href == "" {
|
||||
sb.WriteString(renderInlineContent(linkContent, insideLink))
|
||||
@ -656,6 +658,30 @@ func renderInlineContent(content []map[string]any, insideLink bool) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// safeLinkURL accepts ordinary web, email, telephone, and relative links while
|
||||
// rejecting active-content schemes such as javascript: and data:. Escaping a
|
||||
// URL protects the HTML attribute boundary, but it does not make an unsafe URL
|
||||
// scheme safe to navigate to.
|
||||
func safeLinkURL(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(trimmed)
|
||||
if err != nil || parsed.Scheme == "" {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "http", "https", "mailto", "tel":
|
||||
return trimmed
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// imageChipClass matches the page-builder image block's attribution chip
|
||||
// (backend/blocks/tags via image.ninjatpl in the cms repo) so themes style
|
||||
// blog and page credits identically.
|
||||
@ -700,6 +726,7 @@ func writeAttributionLink(b *strings.Builder, href, label string) {
|
||||
// Trailing sentence punctuation (.,;:!?'") is excluded from the linked URL.
|
||||
// Closing parens, brackets and braces are kept inside the URL only when
|
||||
// balanced with an opener inside the URL itself — so
|
||||
//
|
||||
// "(see https://example.com)" links only "https://example.com"
|
||||
// "https://en.wikipedia.org/wiki/Foo_(bar)" keeps the trailing paren.
|
||||
func autolinkText(text string) string {
|
||||
|
||||
@ -183,6 +183,27 @@ func TestBlockNoteToHTML_NoNestedAnchorInsideExplicitLink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockNoteToHTML_RejectsActiveContentExplicitLink(t *testing.T) {
|
||||
doc := map[string]any{
|
||||
"blocks": []any{
|
||||
map[string]any{
|
||||
"type": "paragraph",
|
||||
"content": []any{
|
||||
map[string]any{
|
||||
"type": "link",
|
||||
"href": " javascript:alert(1) ",
|
||||
"content": []any{map[string]any{"type": "text", "text": "read this"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
html := BlockNoteToHTML(context.Background(), doc)
|
||||
if html != "<p class=\"my-4\">read this</p>\n" {
|
||||
t.Fatalf("unsafe link should render as plain text: %q", html)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockNoteToHTML_NoAutolinkInsideCodeStyle(t *testing.T) {
|
||||
doc := map[string]any{
|
||||
"blocks": []any{
|
||||
|
||||
@ -236,6 +236,28 @@ func TestFileBlock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileBlockRejectsActiveContentURL(t *testing.T) {
|
||||
doc := map[string]any{
|
||||
"blocks": []any{
|
||||
map[string]any{
|
||||
"type": "file",
|
||||
"props": map[string]any{
|
||||
"url": "javascript:alert(document.domain)",
|
||||
"name": "Unsafe link",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
html := BlockNoteToHTML(context.Background(), doc)
|
||||
|
||||
if strings.Contains(html, "href=") || strings.Contains(html, "javascript:") {
|
||||
t.Fatalf("unsafe file URL rendered as a link: %s", html)
|
||||
}
|
||||
if !strings.Contains(html, "Unsafe link") {
|
||||
t.Fatalf("file label should remain visible as plain text: %s", html)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileBlockNoName(t *testing.T) {
|
||||
doc := map[string]any{
|
||||
"blocks": []any{
|
||||
@ -708,6 +730,18 @@ func TestImageBlockAltAndLink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageBlockRejectsActiveContentLink(t *testing.T) {
|
||||
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
|
||||
"url": "/media/x.webp", "alt": "Sunset", "link": "JaVaScRiPt:alert(1)",
|
||||
}))
|
||||
if strings.Contains(html, "<a ") || strings.Contains(strings.ToLower(html), "javascript:") {
|
||||
t.Fatalf("unsafe image link rendered: %s", html)
|
||||
}
|
||||
if !strings.Contains(html, `<img src="/media/x.webp"`) {
|
||||
t.Fatalf("image should still render without its unsafe link: %s", html)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageBlockWidthAndAlign(t *testing.T) {
|
||||
html := BlockNoteToHTML(context.Background(), imageBlock(map[string]any{
|
||||
"url": "/media/x.webp", "width": "medium", "align": "right",
|
||||
|
||||
@ -3,6 +3,8 @@ package templates
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/document"
|
||||
)
|
||||
|
||||
// PageDocument is a full-page template's contribution to the document. The
|
||||
@ -17,6 +19,8 @@ type PageDocument struct {
|
||||
HTMLAttrs map[string]string // extra <html> attributes (e.g. data-theme)
|
||||
HeadExtra HTMLComponent // appended inside <head> after bn.Head
|
||||
BodyEndExtra HTMLComponent // appended before </body> after bn.BodyEnd
|
||||
StatusCode int32 // requested HTTP status; host accepts 200–599
|
||||
Metadata *document.Metadata
|
||||
}
|
||||
|
||||
func (p *PageDocument) Render(ctx context.Context, w io.Writer) error {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user