diff --git a/frontend/wizard-vue/src/components/RadioButtons.vue b/frontend/wizard-vue/src/components/RadioButtons.vue new file mode 100644 index 0000000..2a59e47 --- /dev/null +++ b/frontend/wizard-vue/src/components/RadioButtons.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/wizard-vue/src/components/SpecsForm.vue b/frontend/wizard-vue/src/components/SpecsForm.vue index 54ea8a7..e6bc0b7 100644 --- a/frontend/wizard-vue/src/components/SpecsForm.vue +++ b/frontend/wizard-vue/src/components/SpecsForm.vue @@ -1,12 +1,19 @@ @@ -16,12 +23,22 @@ const groups = groupBy(fields, item => item.group || ''); @@ -29,6 +46,6 @@ const groups = groupBy(fields, item => item.group || ''); diff --git a/frontend/wizard-vue/src/stores/wizard.ts b/frontend/wizard-vue/src/stores/wizard.ts index ddd97b2..18beaee 100644 --- a/frontend/wizard-vue/src/stores/wizard.ts +++ b/frontend/wizard-vue/src/stores/wizard.ts @@ -1,5 +1,5 @@ import {defineStore} from "pinia"; -import {emptySpecs, type SpecField, fields, type Specs} from "@/urlmaker/specs.ts"; +import {emptySpecs, type SpecField, fields, type Specs, type SpecValue} from "@/urlmaker/specs.ts"; import {computed, reactive} from "vue"; import {debounce} from "es-toolkit"; @@ -22,7 +22,7 @@ export const useWizardStore = defineStore('wizard', () => { localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(specs)); }, 100); - function updateSpec(fieldName: keyof Specs, newValue: string) { + function updateSpec(fieldName: keyof Specs, newValue: SpecValue) { specs[fieldName] = newValue; updateLocalStorage(); } diff --git a/frontend/wizard-vue/src/urlmaker/proto/specs.ts b/frontend/wizard-vue/src/urlmaker/proto/specs.ts index d644aeb..b177465 100644 --- a/frontend/wizard-vue/src/urlmaker/proto/specs.ts +++ b/frontend/wizard-vue/src/urlmaker/proto/specs.ts @@ -20,6 +20,7 @@ export namespace rssalchemy { selector_author?: string; selector_created?: string; created_extract_from?: ExtractFrom; + created_attribute_name?: string; selector_content?: string; selector_enclosure?: string; cache_lifetime?: string; @@ -51,6 +52,9 @@ export namespace rssalchemy { if ("created_extract_from" in data && data.created_extract_from != undefined) { this.created_extract_from = data.created_extract_from; } + if ("created_attribute_name" in data && data.created_attribute_name != undefined) { + this.created_attribute_name = data.created_attribute_name; + } if ("selector_content" in data && data.selector_content != undefined) { this.selector_content = data.selector_content; } @@ -110,6 +114,12 @@ export namespace rssalchemy { set created_extract_from(value: ExtractFrom) { pb_1.Message.setField(this, 11, value); } + get created_attribute_name() { + return pb_1.Message.getFieldWithDefault(this, 12, "") as string; + } + set created_attribute_name(value: string) { + pb_1.Message.setField(this, 12, value); + } get selector_content() { return pb_1.Message.getFieldWithDefault(this, 8, "") as string; } @@ -137,6 +147,7 @@ export namespace rssalchemy { selector_author?: string; selector_created?: string; created_extract_from?: ExtractFrom; + created_attribute_name?: string; selector_content?: string; selector_enclosure?: string; cache_lifetime?: string; @@ -166,6 +177,9 @@ export namespace rssalchemy { if (data.created_extract_from != null) { message.created_extract_from = data.created_extract_from; } + if (data.created_attribute_name != null) { + message.created_attribute_name = data.created_attribute_name; + } if (data.selector_content != null) { message.selector_content = data.selector_content; } @@ -187,6 +201,7 @@ export namespace rssalchemy { selector_author?: string; selector_created?: string; created_extract_from?: ExtractFrom; + created_attribute_name?: string; selector_content?: string; selector_enclosure?: string; cache_lifetime?: string; @@ -215,6 +230,9 @@ export namespace rssalchemy { if (this.created_extract_from != null) { data.created_extract_from = this.created_extract_from; } + if (this.created_attribute_name != null) { + data.created_attribute_name = this.created_attribute_name; + } if (this.selector_content != null) { data.selector_content = this.selector_content; } @@ -246,6 +264,8 @@ export namespace rssalchemy { writer.writeString(7, this.selector_created); if (this.created_extract_from != ExtractFrom.InnerText) writer.writeEnum(11, this.created_extract_from); + if (this.created_attribute_name.length) + writer.writeString(12, this.created_attribute_name); if (this.selector_content.length) writer.writeString(8, this.selector_content); if (this.selector_enclosure.length) @@ -285,6 +305,9 @@ export namespace rssalchemy { case 11: message.created_extract_from = reader.readEnum(); break; + case 12: + message.created_attribute_name = reader.readString(); + break; case 8: message.selector_content = reader.readString(); break; diff --git a/frontend/wizard-vue/src/urlmaker/specs.ts b/frontend/wizard-vue/src/urlmaker/specs.ts index 24a459b..cfec895 100644 --- a/frontend/wizard-vue/src/urlmaker/specs.ts +++ b/frontend/wizard-vue/src/urlmaker/specs.ts @@ -1,4 +1,5 @@ import { + validateAttribute, validateDuration, validateSelector, validateUrl, @@ -16,10 +17,16 @@ export enum InputType { Radio = 'radio' } +export type EnumValue = { + label: string + value: number +} +export type Enum = EnumValue[] + export interface SpecField { name: keyof Specs input_type: InputType - enum_values?: {[k: number]: string} + enum?: Enum, label: string default: SpecValue validate: validator @@ -84,13 +91,25 @@ export const fields: SpecField[] = [ { name: 'created_extract_from', input_type: InputType.Radio, - enum_values: rssalchemy.ExtractFrom, + enum: [ + {label: 'Inner Text', value: rssalchemy.ExtractFrom.InnerText}, + {label: 'Attribute', value: rssalchemy.ExtractFrom.Attribute}, + ], label: 'Extract from', default: rssalchemy.ExtractFrom.InnerText, validate: value => Object.values(rssalchemy.ExtractFrom).includes(value), group: 'created', show_if: specs => !!specs.selector_created, }, + { + name: 'created_attribute_name', + input_type: InputType.Text, + label: 'Attribute name', + default: '', + validate: validateAttribute, + show_if: specs => specs.created_extract_from === rssalchemy.ExtractFrom.Attribute, + group: 'created', + }, { name: 'selector_content', diff --git a/frontend/wizard-vue/src/urlmaker/validators.ts b/frontend/wizard-vue/src/urlmaker/validators.ts index 62015f4..efc5e1f 100644 --- a/frontend/wizard-vue/src/urlmaker/validators.ts +++ b/frontend/wizard-vue/src/urlmaker/validators.ts @@ -26,6 +26,10 @@ export function validateSelector(s: SpecValue): boolean { } } +export function validateAttribute(s: SpecValue): boolean { + return /([^\t\n\f \/>"'=]+)/.test(s as string); +} + export function validateDuration(s: SpecValue): boolean { return /^\d+[smh]$/.test(s as string); } diff --git a/internal/api/http/pb/specs.pb.go b/internal/api/http/pb/specs.pb.go index a69aa41..554a6cb 100644 --- a/internal/api/http/pb/specs.pb.go +++ b/internal/api/http/pb/specs.pb.go @@ -69,20 +69,21 @@ func (ExtractFrom) EnumDescriptor() ([]byte, []int) { } type Specs struct { - state protoimpl.MessageState `protogen:"open.v1"` - Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url" validate:"url"` - SelectorPost string `protobuf:"bytes,2,opt,name=selector_post,json=selectorPost,proto3" json:"selector_post" validate:"selector"` - SelectorTitle string `protobuf:"bytes,3,opt,name=selector_title,json=selectorTitle,proto3" json:"selector_title" validate:"selector"` - SelectorLink string `protobuf:"bytes,4,opt,name=selector_link,json=selectorLink,proto3" json:"selector_link" validate:"selector"` - SelectorDescription string `protobuf:"bytes,5,opt,name=selector_description,json=selectorDescription,proto3" json:"selector_description" validate:"omitempty,selector"` - SelectorAuthor string `protobuf:"bytes,6,opt,name=selector_author,json=selectorAuthor,proto3" json:"selector_author" validate:"selector"` - SelectorCreated string `protobuf:"bytes,7,opt,name=selector_created,json=selectorCreated,proto3" json:"selector_created" validate:"selector"` - CreatedExtractFrom ExtractFrom `protobuf:"varint,11,opt,name=created_extract_from,json=createdExtractFrom,proto3,enum=rssalchemy.ExtractFrom" json:"created_extract_from"` - SelectorContent string `protobuf:"bytes,8,opt,name=selector_content,json=selectorContent,proto3" json:"selector_content" validate:"omitempty,selector"` - SelectorEnclosure string `protobuf:"bytes,9,opt,name=selector_enclosure,json=selectorEnclosure,proto3" json:"selector_enclosure" validate:"selector"` - CacheLifetime string `protobuf:"bytes,10,opt,name=cache_lifetime,json=cacheLifetime,proto3" json:"cache_lifetime"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url" validate:"url"` + SelectorPost string `protobuf:"bytes,2,opt,name=selector_post,json=selectorPost,proto3" json:"selector_post" validate:"selector"` + SelectorTitle string `protobuf:"bytes,3,opt,name=selector_title,json=selectorTitle,proto3" json:"selector_title" validate:"selector"` + SelectorLink string `protobuf:"bytes,4,opt,name=selector_link,json=selectorLink,proto3" json:"selector_link" validate:"selector"` + SelectorDescription string `protobuf:"bytes,5,opt,name=selector_description,json=selectorDescription,proto3" json:"selector_description" validate:"omitempty,selector"` + SelectorAuthor string `protobuf:"bytes,6,opt,name=selector_author,json=selectorAuthor,proto3" json:"selector_author" validate:"selector"` + SelectorCreated string `protobuf:"bytes,7,opt,name=selector_created,json=selectorCreated,proto3" json:"selector_created" validate:"selector"` + CreatedExtractFrom ExtractFrom `protobuf:"varint,11,opt,name=created_extract_from,json=createdExtractFrom,proto3,enum=rssalchemy.ExtractFrom" json:"created_extract_from"` + CreatedAttributeName string `protobuf:"bytes,12,opt,name=created_attribute_name,json=createdAttributeName,proto3" json:"created_attribute_name"` + SelectorContent string `protobuf:"bytes,8,opt,name=selector_content,json=selectorContent,proto3" json:"selector_content" validate:"omitempty,selector"` + SelectorEnclosure string `protobuf:"bytes,9,opt,name=selector_enclosure,json=selectorEnclosure,proto3" json:"selector_enclosure" validate:"selector"` + CacheLifetime string `protobuf:"bytes,10,opt,name=cache_lifetime,json=cacheLifetime,proto3" json:"cache_lifetime"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Specs) Reset() { @@ -171,6 +172,13 @@ func (x *Specs) GetCreatedExtractFrom() ExtractFrom { return ExtractFrom_InnerText } +func (x *Specs) GetCreatedAttributeName() string { + if x != nil { + return x.CreatedAttributeName + } + return "" +} + func (x *Specs) GetSelectorContent() string { if x != nil { return x.SelectorContent @@ -198,7 +206,7 @@ var file_proto_specs_proto_rawDesc = string([]byte{ 0x0a, 0x11, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x70, 0x65, 0x63, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x72, 0x73, 0x73, 0x61, 0x6c, 0x63, 0x68, 0x65, 0x6d, 0x79, 0x1a, 0x13, 0x74, 0x61, 0x67, 0x67, 0x65, 0x72, 0x2f, 0x74, 0x61, 0x67, 0x67, 0x65, 0x72, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdc, 0x07, 0x0a, 0x05, 0x53, 0x70, 0x65, 0x63, 0x73, 0x12, 0x30, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb6, 0x08, 0x0a, 0x05, 0x53, 0x70, 0x65, 0x63, 0x73, 0x12, 0x30, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0x9a, 0x84, 0x9e, 0x03, 0x19, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x75, 0x72, 0x6c, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x22, 0x75, 0x72, 0x6c, 0x22, 0x52, 0x03, 0x75, 0x72, 0x6c, @@ -243,28 +251,34 @@ var file_proto_specs_proto_rawDesc = string([]byte{ 0x42, 0x20, 0x9a, 0x84, 0x9e, 0x03, 0x1b, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x22, 0x52, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x45, 0x78, 0x74, 0x72, 0x61, - 0x63, 0x74, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x65, 0x0a, 0x10, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x3a, 0x9a, 0x84, 0x9e, 0x03, 0x35, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x73, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x20, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x22, 0x6f, 0x6d, 0x69, 0x74, 0x65, 0x6d, 0x70, - 0x74, 0x79, 0x2c, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x52, 0x0f, 0x73, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x61, 0x0a, - 0x12, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x73, - 0x75, 0x72, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x42, 0x32, 0x9a, 0x84, 0x9e, 0x03, 0x2d, - 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x65, - 0x6e, 0x63, 0x6c, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x3a, 0x22, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x52, 0x11, 0x73, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x45, 0x6e, 0x63, 0x6c, 0x6f, 0x73, 0x75, 0x72, 0x65, - 0x12, 0x41, 0x0a, 0x0e, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1a, 0x9a, 0x84, 0x9e, 0x03, 0x15, 0x6a, - 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x74, - 0x69, 0x6d, 0x65, 0x22, 0x52, 0x0d, 0x63, 0x61, 0x63, 0x68, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x74, - 0x69, 0x6d, 0x65, 0x2a, 0x2b, 0x0a, 0x0b, 0x45, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x46, 0x72, - 0x6f, 0x6d, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x54, 0x65, 0x78, 0x74, 0x10, - 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x10, 0x01, - 0x42, 0x16, 0x5a, 0x14, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x74, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x58, 0x0a, 0x16, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x42, 0x22, 0x9a, 0x84, 0x9e, 0x03, 0x1d, 0x6a, 0x73, 0x6f, + 0x6e, 0x3a, 0x22, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x52, 0x14, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x65, 0x0a, 0x10, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x3a, 0x9a, 0x84, 0x9e, 0x03, + 0x35, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x3a, 0x22, 0x6f, 0x6d, 0x69, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2c, 0x73, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x52, 0x0f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x61, 0x0a, 0x12, 0x73, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x5f, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x32, 0x9a, 0x84, 0x9e, 0x03, 0x2d, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, + 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x73, 0x75, + 0x72, 0x65, 0x22, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x3a, 0x22, 0x73, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x52, 0x11, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x45, 0x6e, 0x63, 0x6c, 0x6f, 0x73, 0x75, 0x72, 0x65, 0x12, 0x41, 0x0a, 0x0e, 0x63, 0x61, + 0x63, 0x68, 0x65, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x09, 0x42, 0x1a, 0x9a, 0x84, 0x9e, 0x03, 0x15, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x22, 0x63, + 0x61, 0x63, 0x68, 0x65, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x52, 0x0d, + 0x63, 0x61, 0x63, 0x68, 0x65, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x2a, 0x2b, 0x0a, + 0x0b, 0x45, 0x78, 0x74, 0x72, 0x61, 0x63, 0x74, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x0d, 0x0a, 0x09, + 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x54, 0x65, 0x78, 0x74, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x10, 0x01, 0x42, 0x16, 0x5a, 0x14, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2f, + 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/proto/specs.proto b/proto/specs.proto index e034114..29dc311 100644 --- a/proto/specs.proto +++ b/proto/specs.proto @@ -21,6 +21,7 @@ message Specs { string selector_created = 7 [(tagger.tags) = "json:\"selector_created\" validate:\"selector\""]; ExtractFrom created_extract_from = 11 [(tagger.tags) = "json:\"created_extract_from\""]; + string created_attribute_name = 12 [(tagger.tags) = "json:\"created_attribute_name\""]; string selector_content = 8 [(tagger.tags) = "json:\"selector_content\" validate:\"omitempty,selector\""]; string selector_enclosure = 9 [(tagger.tags) = "json:\"selector_enclosure\" validate:\"selector\""];