diff --git a/.eslintrc b/.eslintrc
index 497e93d7..7a7fa57d 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -19,6 +19,7 @@
// Turn on errors for missing imports.
"import/no-unresolved": "error",
"import/named": "off",
+ "no-useless-constructor": "off",
"no-console": [
"off"
]
diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts
index 4327afa6..7c755f66 100644
--- a/src/api/ApiClient.ts
+++ b/src/api/ApiClient.ts
@@ -1,15 +1,13 @@
import { PHPResponse, PlaygroundClient } from '@wp-playground/client';
-import { BlogPostsApi } from '@/api/BlogPosts';
-import { PagesApi } from '@/api/Pages';
import { SettingsApi } from '@/api/Settings';
import { UsersApi } from '@/api/Users';
import { BlueprintsApi } from '@/api/Blueprints';
+import { SubjectsApi } from '@/api/SubjectsApi';
export class ApiClient {
private readonly playgroundClient: PlaygroundClient;
private readonly _siteUrl: string;
- private readonly _blogPosts: BlogPostsApi;
- private readonly _pages: PagesApi;
+ private readonly _subjects: SubjectsApi;
private readonly _settings: SettingsApi;
private readonly _users: UsersApi;
private readonly _blueprints: BlueprintsApi;
@@ -18,8 +16,7 @@ export class ApiClient {
this.playgroundClient = playgroundClient;
this._siteUrl = siteUrl;
this._blueprints = new BlueprintsApi( this );
- this._blogPosts = new BlogPostsApi( this );
- this._pages = new PagesApi( this );
+ this._subjects = new SubjectsApi( this );
this._settings = new SettingsApi( this );
this._users = new UsersApi( this );
}
@@ -32,12 +29,8 @@ export class ApiClient {
return this._blueprints;
}
- get blogPosts(): BlogPostsApi {
- return this._blogPosts;
- }
-
- get pages(): PagesApi {
- return this._pages;
+ get subjects(): SubjectsApi {
+ return this._subjects;
}
get settings(): SettingsApi {
diff --git a/src/api/ApiTypes.ts b/src/api/ApiTypes.ts
index a62a901f..2d5bf1d4 100644
--- a/src/api/ApiTypes.ts
+++ b/src/api/ApiTypes.ts
@@ -6,12 +6,12 @@ export type ApiPost = {
transformedId: number;
previewUrl: string;
sourceUrl: string;
- rawDate: string;
- parsedDate: string;
- rawTitle: string;
- parsedTitle: string;
- rawContent: string;
- parsedContent: string;
+ rawDate?: string;
+ parsedDate?: string;
+ rawTitle?: string;
+ parsedTitle?: string;
+ rawContent?: string;
+ parsedContent?: string;
};
export type ApiPage = ApiPost;
diff --git a/src/api/BlogPosts.ts b/src/api/BlogPosts.ts
deleted file mode 100644
index 8011463b..00000000
--- a/src/api/BlogPosts.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { BlogPost } from '@/model/subject/BlogPost';
-import { ApiClient } from '@/api/ApiClient';
-import { SubjectType } from '@/model/subject/Subject';
-import { newDateField } from '@/model/field/DateField';
-import { newTextField } from '@/model/field/TextField';
-import { newHtmlField } from '@/model/field/HtmlField';
-import { ApiPost } from '@/api/ApiTypes';
-
-export class BlogPostsApi {
- // eslint-disable-next-line no-useless-constructor
- constructor( private readonly client: ApiClient ) {}
-
- async create( blogPost: BlogPost ): Promise< BlogPost > {
- const response = ( await this.client.post( '/blog-posts', {
- sourceUrl: blogPost.sourceUrl,
- } ) ) as ApiPost;
- return fromApiResponse( response );
- }
-
- async update( id: number, post: BlogPost ): Promise< BlogPost > {
- const response = ( await this.client.post(
- `/blog-posts/${ id }`,
- toApiRequest( post )
- ) ) as ApiPost;
- return fromApiResponse( response );
- }
-
- async findById( id: string ): Promise< BlogPost | null > {
- const post = ( await this.client.get(
- '/blog-posts/' + id
- ) ) as ApiPost;
- return post ? fromApiResponse( post ) : null;
- }
-
- async findBySourceUrl( sourceUrl: string ): Promise< BlogPost | null > {
- const post = ( await this.client.get(
- '/blog-posts?sourceurl=' + sourceUrl
- ) ) as ApiPost;
- return post ? fromApiResponse( post ) : null;
- }
-}
-
-function fromApiResponse( response: ApiPost ): BlogPost {
- const date = newDateField( response.rawDate, response.parsedDate );
- const title = newTextField( response.rawTitle, response.parsedTitle ?? '' );
- const content = newHtmlField(
- response.rawContent,
- response.parsedContent ?? ''
- );
-
- return {
- id: response.id,
- type: SubjectType.BlogPost,
- sourceUrl: response.sourceUrl,
- transformedId: response.transformedId,
- previewUrl: response.previewUrl,
- title,
- date,
- content,
- };
-}
-
-function toApiRequest( post: BlogPost ): ApiPost {
- return {
- id: post.id,
- // read-only from api perspective, so including it has no effect
- transformedId: post.transformedId,
- // update not allowed so will be a bad request if you try to do it
- sourceUrl: post.sourceUrl,
- // read-only from api perspective, so including it has no effect
- previewUrl: post.previewUrl,
- rawDate: post.date.rawValue,
- parsedDate: post.date.parsedValue.toISOString(),
- rawTitle: post.title.rawValue,
- parsedTitle: post.title.parsedValue,
- rawContent: post.content.rawValue,
- parsedContent: post.content.parsedValue,
- };
-}
diff --git a/src/api/Blueprints.ts b/src/api/Blueprints.ts
index b833d9d8..6b10b040 100644
--- a/src/api/Blueprints.ts
+++ b/src/api/Blueprints.ts
@@ -1,6 +1,6 @@
import { ApiClient } from '@/api/ApiClient';
-import { Blueprint } from '@/model/blueprint/Blueprint';
-import { SubjectType } from '@/model/subject/Subject';
+import { Blueprint } from '@/model/Blueprint';
+import { SubjectType } from '@/model/Subject';
export class BlueprintsApi {
// eslint-disable-next-line no-useless-constructor
diff --git a/src/api/Pages.ts b/src/api/Pages.ts
deleted file mode 100644
index e1f74486..00000000
--- a/src/api/Pages.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { Page } from '@/model/subject/Page';
-import { ApiClient } from '@/api/ApiClient';
-import { SubjectType } from '@/model/subject/Subject';
-import { newTextField } from '@/model/field/TextField';
-import { newHtmlField } from '@/model/field/HtmlField';
-import { ApiPage } from '@/api/ApiTypes';
-import { newDateField } from '@/model/field/DateField';
-
-export class PagesApi {
- // eslint-disable-next-line no-useless-constructor
- constructor( private readonly client: ApiClient ) {}
-
- async create( page: Page ): Promise< Page > {
- const response = ( await this.client.post( '/pages', {
- sourceUrl: page.sourceUrl,
- } ) ) as ApiPage;
- return fromApiResponse( response );
- }
-
- async update( id: number, page: Page ): Promise< Page > {
- const response = ( await this.client.post(
- `/pages/${ id }`,
- toApiRequest( page )
- ) ) as ApiPage;
- return fromApiResponse( response );
- }
-
- async findById( id: string ): Promise< Page | null > {
- const page = ( await this.client.get( '/pages/' + id ) ) as ApiPage;
- return page ? fromApiResponse( page ) : null;
- }
-
- async findBySourceUrl( sourceUrl: string ): Promise< Page | null > {
- const page = ( await this.client.get(
- '/pages?sourceurl=' + sourceUrl
- ) ) as ApiPage;
- return page ? fromApiResponse( page ) : null;
- }
-}
-
-function fromApiResponse( response: ApiPage ): Page {
- const date = newDateField( response.rawDate, response.parsedDate );
- const title = newTextField( response.rawTitle, response.parsedTitle ?? '' );
- const content = newHtmlField(
- response.rawContent,
- response.parsedContent ?? ''
- );
-
- return {
- id: response.id,
- type: SubjectType.Page,
- sourceUrl: response.sourceUrl,
- transformedId: response.transformedId,
- previewUrl: response.previewUrl,
- title,
- date,
- content,
- };
-}
-
-function toApiRequest( page: Page ): ApiPage {
- return {
- id: page.id,
- // read-only from api perspective, so including it has no effect
- transformedId: page.transformedId,
- // update not allowed so will be a bad request if you try to do it
- sourceUrl: page.sourceUrl,
- // read-only from api perspective, so including it has no effect
- previewUrl: page.previewUrl,
- rawDate: page.date.rawValue,
- parsedDate: page.date.parsedValue.toISOString(),
- rawTitle: page.title.rawValue,
- parsedTitle: page.title.parsedValue,
- rawContent: page.content.rawValue,
- parsedContent: page.content.parsedValue,
- };
-}
diff --git a/src/api/SubjectsApi.ts b/src/api/SubjectsApi.ts
new file mode 100644
index 00000000..24091e10
--- /dev/null
+++ b/src/api/SubjectsApi.ts
@@ -0,0 +1,111 @@
+import { ApiClient } from '@/api/ApiClient';
+import { Subject, SubjectType } from '@/model/Subject';
+import { ApiPost } from '@/api/ApiTypes';
+import { newDateField } from '@/model/field/DateField';
+import { newTextField } from '@/model/field/TextField';
+import { newHtmlField } from '@/model/field/HtmlField';
+
+const endpoints = new Map< string, string >( [
+ [ SubjectType.BlogPost, '/blog-posts' ],
+ [ SubjectType.Page, '/pages' ],
+] );
+
+export class SubjectsApi {
+ constructor( private readonly client: ApiClient ) {}
+
+ async create( type: SubjectType, sourceUrl: string ): Promise< Subject > {
+ const path = getEndpoint( type );
+ const response = ( await this.client.post( path, {
+ sourceUrl,
+ } ) ) as ApiPost;
+ return fromApiResponse( response );
+ }
+
+ async update( subject: Subject ): Promise< Subject > {
+ const path = `${ getEndpoint( subject.type ) }/${ subject.id }`;
+ const response = ( await this.client.post(
+ path,
+ toApiUpdateRequest( subject )
+ ) ) as ApiPost;
+ return fromApiResponse( response );
+ }
+
+ async findById( type: SubjectType, id: number ): Promise< Subject | null > {
+ const path = `${ getEndpoint( type ) }/${ id }`;
+ const post = ( await this.client.get( path ) ) as ApiPost;
+ return post ? fromApiResponse( post ) : null;
+ }
+
+ async findBySourceUrl(
+ type: SubjectType,
+ sourceUrl: string
+ ): Promise< Subject | null > {
+ const path = `${ getEndpoint( type ) }?sourceurl=${ sourceUrl }`;
+ const post = ( await this.client.get( path ) ) as ApiPost;
+ return post ? fromApiResponse( post ) : null;
+ }
+}
+
+function getEndpoint( type: SubjectType ): string {
+ const endpoint = endpoints.get( type );
+ if ( ! endpoint ) {
+ throw Error( `unknown endpoint: ${ type }` );
+ }
+ return endpoint;
+}
+
+function fromApiResponse( response: ApiPost ): Subject {
+ const date = newDateField( response.rawDate, response.parsedDate );
+ const title = newTextField( response.rawTitle, response.parsedTitle ?? '' );
+ const content = newHtmlField(
+ response.rawContent,
+ response.parsedContent ?? ''
+ );
+
+ return {
+ id: response.id,
+ type: SubjectType.BlogPost,
+ sourceUrl: response.sourceUrl,
+ transformedId: response.transformedId,
+ previewUrl: response.previewUrl,
+ fields: {
+ title,
+ date,
+ content,
+ },
+ };
+}
+
+type UpdateBody = Omit< ApiPost, 'sourceUrl' | 'transformedId' | 'previewUrl' >;
+
+function toApiUpdateRequest( subject: Subject ): UpdateBody {
+ let request: UpdateBody = {
+ id: subject.id,
+ };
+
+ if ( subject.fields.date ) {
+ request = {
+ ...request,
+ rawDate: subject.fields.date.rawValue,
+ parsedDate: subject.fields.date.parsedValue.toISOString(),
+ };
+ }
+
+ if ( subject.fields.title ) {
+ request = {
+ ...request,
+ rawTitle: subject.fields.title.rawValue,
+ parsedTitle: subject.fields.title.parsedValue,
+ };
+ }
+
+ if ( subject.fields.content ) {
+ request = {
+ ...request,
+ rawContent: subject.fields.content.rawValue,
+ parsedContent: subject.fields.content.parsedValue,
+ };
+ }
+
+ return request;
+}
diff --git a/src/model/Blueprint.ts b/src/model/Blueprint.ts
new file mode 100644
index 00000000..0bf0f5a0
--- /dev/null
+++ b/src/model/Blueprint.ts
@@ -0,0 +1,33 @@
+import { SubjectType } from '@/model/Subject';
+
+export interface Blueprint {
+ type: SubjectType;
+ id: string; // TODO: Probably need to make this a number when we start storing Blueprints on the backend.
+ sourceUrl: string;
+ valid: boolean;
+ selectors: Record< string, string >;
+}
+
+export function newBlueprint(
+ type: SubjectType,
+ sourceUrl: string
+): Blueprint {
+ return {
+ id: '',
+ type,
+ sourceUrl,
+ valid: false,
+ selectors: {},
+ };
+}
+
+export function validateBlueprint( blueprint: Blueprint ): boolean {
+ let isValid = true;
+ for ( const selector of Object.values( blueprint.selectors ) ) {
+ if ( selector === '' ) {
+ isValid = false;
+ break;
+ }
+ }
+ return isValid;
+}
diff --git a/src/model/subject/Schema.ts b/src/model/Schema.ts
similarity index 84%
rename from src/model/subject/Schema.ts
rename to src/model/Schema.ts
index 3c129d38..c6f9dda7 100644
--- a/src/model/subject/Schema.ts
+++ b/src/model/Schema.ts
@@ -1,6 +1,6 @@
// eslint-disable-next-line import/no-unresolved
import Schema from '@schema/schema.json';
-import { SubjectType } from '@/model/subject/Subject';
+import { SubjectType } from '@/model/Subject';
export function getSchema( subjectType: SubjectType ) {
if ( ! Schema.hasOwnProperty( subjectType ) ) {
diff --git a/src/model/Subject.ts b/src/model/Subject.ts
new file mode 100644
index 00000000..9b4c4bb7
--- /dev/null
+++ b/src/model/Subject.ts
@@ -0,0 +1,26 @@
+import { Field } from '@/model/field/Field';
+
+export enum SubjectType {
+ BlogPost = 'blog-post',
+ Page = 'page',
+}
+
+export interface Subject {
+ type: SubjectType;
+ id: number;
+ transformedId: number;
+ sourceUrl: string;
+ previewUrl: string;
+ fields: Record< string, Field >;
+}
+
+export function validateFields( subject: Subject ): boolean {
+ let isValid = true;
+ Object.keys( subject.fields ).forEach( ( key ) => {
+ const f = subject.fields[ key ];
+ if ( f.rawValue === '' || f.parsedValue === '' ) {
+ isValid = false;
+ }
+ } );
+ return isValid;
+}
diff --git a/src/model/blueprint/BlogPost.ts b/src/model/blueprint/BlogPost.ts
deleted file mode 100644
index 9134fc5f..00000000
--- a/src/model/blueprint/BlogPost.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { SubjectType } from '@/model/subject/Subject';
-import { FieldType } from '@/model/field/Field';
-import { Blueprint } from '@/model/blueprint/Blueprint';
-
-export interface BlogPostBlueprint extends Blueprint {
- type: SubjectType.BlogPost;
- fields: {
- title: { type: FieldType.Text; selector?: string };
- date: { type: FieldType.Date; selector?: string };
- content: { type: FieldType.Html; selector?: string };
- };
-}
-
-export function newBlogPostBlueprint( sourceUrl: string ): BlogPostBlueprint {
- return {
- id: '',
- type: SubjectType.BlogPost,
- sourceUrl,
- valid: false,
- fields: {
- date: {
- type: FieldType.Date,
- selector: '',
- },
- title: {
- type: FieldType.Text,
- selector: '',
- },
- content: {
- type: FieldType.Html,
- selector: '',
- },
- },
- };
-}
-
-export function validateBlogpostBlueprint(
- blueprint: BlogPostBlueprint
-): boolean {
- let isValid = true;
- for ( const f of Object.values( blueprint.fields ) ) {
- if ( f.selector === '' ) {
- isValid = false;
- break;
- }
- }
- return isValid;
-}
diff --git a/src/model/blueprint/Blueprint.ts b/src/model/blueprint/Blueprint.ts
deleted file mode 100644
index 4c232874..00000000
--- a/src/model/blueprint/Blueprint.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { SubjectType } from '@/model/subject/Subject';
-import { FieldType } from '@/model/field/Field';
-
-export interface BlueprintField {
- type: FieldType;
- selector?: string;
-}
-
-export interface Blueprint {
- type: SubjectType;
- id: string; // TODO: Probably need to make this a number when we start storing Blueprints on the backend.
- sourceUrl: string;
- valid: boolean;
- fields: Record< string, BlueprintField >;
-}
diff --git a/src/model/blueprint/Page.ts b/src/model/blueprint/Page.ts
deleted file mode 100644
index c27c5a9e..00000000
--- a/src/model/blueprint/Page.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { SubjectType } from '@/model/subject/Subject';
-import { FieldType } from '@/model/field/Field';
-import { Blueprint } from '@/model/blueprint/Blueprint';
-
-export interface PageBlueprint extends Blueprint {
- type: SubjectType.Page;
- fields: {
- title: { type: FieldType.Text; selector?: string };
- date: { type: FieldType.Date; selector?: string };
- content: { type: FieldType.Html; selector?: string };
- };
-}
-
-export function newPageBlueprint( sourceUrl: string ): PageBlueprint {
- return {
- id: '',
- type: SubjectType.Page,
- sourceUrl,
- valid: false,
- fields: {
- date: {
- type: FieldType.Date,
- selector: '',
- },
- title: {
- type: FieldType.Text,
- selector: '',
- },
- content: {
- type: FieldType.Html,
- selector: '',
- },
- },
- };
-}
-
-export function validatePageBlueprint( blueprint: PageBlueprint ): boolean {
- let isValid = true;
- for ( const f of Object.values( blueprint.fields ) ) {
- if ( f.selector === '' ) {
- isValid = false;
- break;
- }
- }
- return isValid;
-}
diff --git a/src/model/subject/BlogPost.ts b/src/model/subject/BlogPost.ts
deleted file mode 100644
index f4ed9734..00000000
--- a/src/model/subject/BlogPost.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Subject, SubjectType } from '@/model/subject/Subject';
-import { newTextField, TextField } from '@/model/field/TextField';
-import { HtmlField, newHtmlField } from '@/model/field/HtmlField';
-import { DateField, newDateField } from '@/model/field/DateField';
-
-export interface BlogPost extends Subject {
- type: SubjectType.BlogPost;
- date: DateField;
- title: TextField;
- content: HtmlField;
-}
-
-export function newBlogPost( sourceUrl: string ): BlogPost {
- return {
- id: 0,
- type: SubjectType.BlogPost,
- transformedId: 0,
- previewUrl: '',
- sourceUrl,
- date: newDateField(),
- title: newTextField(),
- content: newHtmlField(),
- };
-}
-
-export function validateBlogPost( blogPost: BlogPost ): boolean {
- const fields = [ blogPost.title, blogPost.date, blogPost.content ];
- let isValid = true;
- for ( const f of fields ) {
- if ( f.rawValue === '' || f.parsedValue === '' ) {
- isValid = false;
- break;
- }
- }
- return isValid;
-}
diff --git a/src/model/subject/Page.ts b/src/model/subject/Page.ts
deleted file mode 100644
index 7e08e4e6..00000000
--- a/src/model/subject/Page.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Subject, SubjectType } from '@/model/subject/Subject';
-import { newTextField, TextField } from '@/model/field/TextField';
-import { HtmlField, newHtmlField } from '@/model/field/HtmlField';
-import { DateField, newDateField } from '@/model/field/DateField';
-
-export interface Page extends Subject {
- type: SubjectType.Page;
- date: DateField;
- title: TextField;
- content: HtmlField;
-}
-
-export function newPage( sourceUrl: string ): Page {
- return {
- id: 0,
- transformedId: 0,
- previewUrl: '',
- type: SubjectType.Page,
- sourceUrl,
- date: newDateField(),
- title: newTextField(),
- content: newHtmlField(),
- };
-}
-
-export function validatePage( page: Page ): boolean {
- const fields = [ page.title, page.date, page.content ];
- let isValid = true;
- for ( const f of fields ) {
- if ( f.rawValue === '' || f.parsedValue === '' ) {
- isValid = false;
- break;
- }
- }
- return isValid;
-}
diff --git a/src/model/subject/Subject.ts b/src/model/subject/Subject.ts
deleted file mode 100644
index 665e66d1..00000000
--- a/src/model/subject/Subject.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export enum SubjectType {
- BlogPost = 'blog-post',
- Page = 'page',
-}
-
-export const humanReadableSubjectType: Map< SubjectType, string > = new Map( [
- [ SubjectType.BlogPost, 'Blog Post' ],
- [ SubjectType.Page, 'Page' ],
-] );
-
-export interface Subject {
- type: SubjectType;
- id: number;
- transformedId: number;
- sourceUrl: string;
- previewUrl: string;
-}
diff --git a/src/parser/blog-post.ts b/src/parser/blog-post.ts
deleted file mode 100644
index a2d10663..00000000
--- a/src/parser/blog-post.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { pasteHandler, serialize } from '@wordpress/blocks';
-import { findDeepestChild } from '@/parser/util';
-import { DateField, newDateField } from '@/model/field/DateField';
-import { newTextField, TextField } from '@/model/field/TextField';
-import { HtmlField, newHtmlField } from '@/model/field/HtmlField';
-import { Field } from '@/model/field/Field';
-
-export function parseBlogPostField( name: string, field: Field ): Field {
- switch ( name ) {
- case 'date':
- return parseBlogPostDate( field.rawValue );
- case 'title':
- return parseBlogPostTitle( field.rawValue );
- case 'content':
- return parseBlogPostContent( field.rawValue );
- default:
- throw Error( `unknown field type ${ field.type }` );
- }
-}
-
-export function parseBlogPostDate( html: string ): DateField {
- const container = document.createElement( 'div' );
- container.innerHTML = html.trim();
- const element = container.querySelector( 'time' );
- return newDateField( html, element ? element.dateTime : '' );
-}
-
-export function parseBlogPostTitle( html: string ): TextField {
- const deepestChild = findDeepestChild( html );
- return newTextField( html, deepestChild?.innerHTML ?? '' );
-}
-
-export function parseBlogPostContent( html: string ): HtmlField {
- return newHtmlField( html, serializeBlocks( html ) );
-}
-
-function serializeBlocks( html: string ): string {
- const blocks = pasteHandler( {
- mode: 'BLOCKS',
- HTML: html,
- } );
- return serialize( blocks );
-}
diff --git a/src/parser/field.ts b/src/parser/field.ts
new file mode 100644
index 00000000..bafa3a72
--- /dev/null
+++ b/src/parser/field.ts
@@ -0,0 +1,34 @@
+import { Field, FieldType } from '@/model/field/Field';
+import { DateField, newDateField } from '@/model/field/DateField';
+import { newTextField, TextField } from '@/model/field/TextField';
+import { findDeepestChild, htmlToBlocks } from '@/parser/util';
+import { HtmlField, newHtmlField } from '@/model/field/HtmlField';
+
+export function parseField( field: Field ): Field {
+ switch ( field.type ) {
+ case FieldType.Date:
+ return parseDate( field.rawValue );
+ case FieldType.Text:
+ return parseText( field.rawValue );
+ case FieldType.Html:
+ return parseHtml( field.rawValue );
+ default:
+ throw Error( `unknown field type ${ field.type }` );
+ }
+}
+
+function parseDate( html: string ): DateField {
+ const container = document.createElement( 'div' );
+ container.innerHTML = html.trim();
+ const element = container.querySelector( 'time' );
+ return newDateField( html, element ? element.dateTime : '' );
+}
+
+function parseText( html: string ): TextField {
+ const deepestChild = findDeepestChild( html );
+ return newTextField( html, deepestChild?.innerHTML ?? '' );
+}
+
+function parseHtml( html: string ): HtmlField {
+ return newHtmlField( html, htmlToBlocks( html ) );
+}
diff --git a/src/parser/page.ts b/src/parser/page.ts
deleted file mode 100644
index c8d2706d..00000000
--- a/src/parser/page.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { pasteHandler, serialize } from '@wordpress/blocks';
-import { findDeepestChild } from '@/parser/util';
-import { newTextField, TextField } from '@/model/field/TextField';
-import { HtmlField, newHtmlField } from '@/model/field/HtmlField';
-import { Field } from '@/model/field/Field';
-import { DateField, newDateField } from '@/model/field/DateField';
-
-export function parsePageField( name: string, field: Field ): Field {
- switch ( name ) {
- case 'date':
- return parsePageDate( field.rawValue );
- case 'title':
- return parsePageTitle( field.rawValue );
- case 'content':
- return parsePageContent( field.rawValue );
- default:
- throw Error( `unknown field type ${ field.type }` );
- }
-}
-
-export function parsePageDate( html: string ): DateField {
- const container = document.createElement( 'div' );
- container.innerHTML = html.trim();
- const element = container.querySelector( 'time' );
- return newDateField( html, element ? element.dateTime : '' );
-}
-
-export function parsePageTitle( html: string ): TextField {
- const deepestChild = findDeepestChild( html );
- return newTextField( html, deepestChild?.innerHTML ?? '' );
-}
-
-export function parsePageContent( html: string ): HtmlField {
- return newHtmlField( html, serializeBlocks( html ) );
-}
-
-function serializeBlocks( html: string ): string {
- const blocks = pasteHandler( {
- mode: 'BLOCKS',
- HTML: html,
- } );
- return serialize( blocks );
-}
diff --git a/src/parser/util.ts b/src/parser/util.ts
index 608fbabd..fa089800 100644
--- a/src/parser/util.ts
+++ b/src/parser/util.ts
@@ -1,3 +1,13 @@
+import { pasteHandler, serialize } from '@wordpress/blocks';
+
+export function htmlToBlocks( html: string ): string {
+ const blocks = pasteHandler( {
+ mode: 'BLOCKS',
+ HTML: html,
+ } );
+ return serialize( blocks );
+}
+
export function findDeepestChild( html: string ): Element | undefined {
const container = document.createElement( 'div' );
container.innerHTML = html.trim();
diff --git a/src/ui/App.tsx b/src/ui/App.tsx
index c9be22df..9331ee8b 100644
--- a/src/ui/App.tsx
+++ b/src/ui/App.tsx
@@ -12,9 +12,9 @@ import {
useRouteLoaderData,
} from 'react-router-dom';
import { StrictMode, useEffect, useState } from 'react';
-import { NewSession } from '@/ui/start/NewSession';
+import { NewSession } from '@/ui/session/NewSession';
import { ViewSession } from '@/ui/session/ViewSession';
-import { Home } from '@/ui/start/Home';
+import { Home } from '@/ui/Home';
import { getConfig, setConfig } from '@/storage/config';
import { getSession, listSessions, Session } from '@/storage/session';
import { PlaceholderPreview } from '@/ui/preview/PlaceholderPreview';
@@ -24,7 +24,7 @@ import { PlaygroundClient } from '@wp-playground/client';
import { Breadcrumbs } from '@/ui/components/Breadcrumbs';
import { NewBlueprint } from '@/ui/blueprints/NewBlueprint';
import { EditBlueprint } from '@/ui/blueprints/EditBlueprint';
-import { SubjectType } from '@/model/subject/Subject';
+import { SubjectType } from '@/model/Subject';
import { ImportWithBlueprint } from '@/ui/import/ImportWithBlueprint';
import { StartPageImport } from '@/ui/import/pages/StartPageImport';
import { SelectNavigation } from '@/ui/import/pages/SelectNavigation';
diff --git a/src/ui/start/Home.tsx b/src/ui/Home.tsx
similarity index 100%
rename from src/ui/start/Home.tsx
rename to src/ui/Home.tsx
diff --git a/src/ui/blueprints/BlueprintEditor.tsx b/src/ui/blueprints/BlueprintEditor.tsx
new file mode 100644
index 00000000..4749762c
--- /dev/null
+++ b/src/ui/blueprints/BlueprintEditor.tsx
@@ -0,0 +1,42 @@
+import { Field } from '@/model/field/Field';
+import { FieldsEditor } from '@/ui/components/FieldsEditor/FieldsEditor';
+import { getSchema } from '@/model/Schema';
+import { Subject } from '@/model/Subject';
+import { Blueprint } from '@/model/Blueprint';
+
+interface Props {
+ blueprint: Blueprint;
+ subject: Subject;
+ onFieldChanged: ( name: string, field: Field, selector: string ) => void;
+}
+
+export function BlueprintEditor( props: Props ) {
+ const { blueprint, subject, onFieldChanged } = props;
+ const schema = getSchema( subject.type );
+ const schemaFields = schema.fields;
+
+ const subjectFields: { name: string; field: Field }[] = [];
+ const selectors: {
+ name: string;
+ selector?: string;
+ }[] = [];
+
+ Object.keys( schemaFields ).forEach( ( name ) => {
+ subjectFields.push( {
+ name,
+ field: subject.fields[ name ],
+ } );
+ selectors.push( {
+ name,
+ selector: blueprint.selectors[ name ],
+ } );
+ } );
+
+ return (
+