@dosgato/templating 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import { PageWithAncestors, ComponentData } from './component';
2
+ import { LinkDefinition } from './links';
3
+ export declare type APITemplateType = 'page' | 'component' | 'data';
4
+ /**
5
+ * This interface lays out the structure the API needs for each template in the system.
6
+ */
7
+ export interface APITemplate {
8
+ type: APITemplateType;
9
+ /**
10
+ * A unique string to globally identify this template across installations. Namespacing like
11
+ * edu.txstate.RichTextEditor could be useful but no special format is required.
12
+ */
13
+ templateKey: string;
14
+ /**
15
+ * Each template must declare its areas and the template keys of components that will be
16
+ * permitted inside each area. The list of allowed component templates can be updated beyond
17
+ * the list provided here. See templateRegistry.addAvailableComponent's comment for info on why.
18
+ */
19
+ areas: Record<string, string[]>;
20
+ /**
21
+ * Each template must provide a list of migrations for upgrading the data schema over time.
22
+ * Typically this will start as an empty array and migrations will be added as the template
23
+ * gets refactored.
24
+ */
25
+ migrations: Migration[];
26
+ /**
27
+ * Each template must provide a function that returns links from its data so that they
28
+ * can be indexed. Only fields that are links need to be returned. Links inside rich editor
29
+ * text will be extracted automatically from any text returned by getFulltext (see below)
30
+ */
31
+ getLinks: LinkGatheringFn;
32
+ /**
33
+ * Each template must provide the text from any text or rich editor data it possesses, so that
34
+ * the text can be decomposed into words and indexed for fulltext searches. Any text returned
35
+ * by this function will also be scanned for links.
36
+ */
37
+ getFulltext: FulltextGatheringFn;
38
+ /**
39
+ * Each template must provide a validation function so that the API can enforce its data is
40
+ * shaped properly. If there are no issues, it should return an empty object {}, otherwise it
41
+ * should return an object with keys that reference the path to the error and values that
42
+ * are an array of error messages pertaining to that path.
43
+ *
44
+ * For instance, if name is required and the user didn't provide one, you would return:
45
+ * { name: ['A name is required.'] }
46
+ *
47
+ * This method is async so that you can do things like look in the database for conflicting
48
+ * names.
49
+ */
50
+ validate: (data: any) => Promise<Record<string, string[]>>;
51
+ /**
52
+ * Hard-coded properties that may be set on page templates to influence the rendering of
53
+ * components on the page. For instance, a set of color choices that are customized for
54
+ * each template design. Components on the page may refer to the color information stored
55
+ * in the template during dialogs and while rendering. Changing to a different page template
56
+ * could then result in different color choices for components like buttons.
57
+ *
58
+ * Must be null for non-page templates.
59
+ */
60
+ templateProperties?: any;
61
+ }
62
+ /**
63
+ * In dosgato CMS, the data in the database is not altered except during user activity. This
64
+ * means that older records could have been saved when the schema expected by component
65
+ * rendering code was different than the date it's being rendered. To handle this, each
66
+ * page and component template is required to provide migrations responsible for
67
+ * transforming the data to the needed schema version.
68
+ *
69
+ * In order to support backwards compatibility, each API client will specify the date
70
+ * when the code was written, so that their assumptions about the schema will be
71
+ * frozen in time. This system means that migrations need to run backward as well as forward
72
+ * in time.
73
+ *
74
+ * The `up` method is for changing data from an older schema to a newer one. The
75
+ * `down` method is for changing data back from the newer schema to the older one.
76
+ * If a `down` method cannot be provided, the migration is considered to be a breaking
77
+ * change and anyone asking to rewind time to before the migration will receive an error.
78
+ *
79
+ * Your `up` and `down` methods will be applied to components in bottom-up fashion, so you
80
+ * can assume that any components inside one of your areas has already been processed.
81
+ */
82
+ export interface Migration {
83
+ createdAt: Date;
84
+ up: (data: ComponentData, page: PageWithAncestors) => ComponentData | Promise<ComponentData>;
85
+ down: (data: ComponentData, page: PageWithAncestors) => ComponentData | Promise<ComponentData>;
86
+ }
87
+ export declare type LinkGatheringFn = (data: any) => LinkDefinition[];
88
+ export declare type FulltextGatheringFn = (data: any) => string[];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,119 @@
1
+ import { Page } from './page';
2
+ import { ResourceProvider } from './provider';
3
+ /**
4
+ * This is the primary templating class to build your templates. Subclass it and provide
5
+ * at least a render function.
6
+ *
7
+ * During rendering, it will be "hydrated" - placed into a full page structure with its
8
+ * parent and child components linked.
9
+ */
10
+ export declare abstract class Component<DataType extends ComponentData = any, FetchedType = any, RenderContextType extends ContextBase = any> extends ResourceProvider {
11
+ static templateKey: string;
12
+ static templateName: string;
13
+ areas: Map<string, Component<any, any, any>[]>;
14
+ data: Omit<DataType, 'areas'>;
15
+ fetched: FetchedType;
16
+ renderCtx: RenderContextType;
17
+ path: string;
18
+ parent?: Component;
19
+ page?: Page;
20
+ hadError: boolean;
21
+ /**
22
+ * The first phase of rendering a component is the fetch phase. Each component may
23
+ * provide a fetch method that looks up data it needs from external sources. This step
24
+ * is FLAT - it will be executed concurrently for all the components on the page for
25
+ * maximum speed.
26
+ *
27
+ * Note that this.page will be available, along with its ancestors property containing
28
+ * all the data from ancestor pages, in case there is a need for inheritance. It is
29
+ * recommended to copy any needed data into the return object, as future phases will not
30
+ * want to resolve the inheritance again.
31
+ */
32
+ fetch(editMode: boolean): Promise<FetchedType>;
33
+ /**
34
+ * The second phase of rendering a component is the context phase. This step is TOP-DOWN,
35
+ * each component will receive the parent component's context, modify it as desired,
36
+ * and then pass context to its children.
37
+ *
38
+ * This is useful for rendering logic that is sensitive to where the component exists in
39
+ * the hierarchy of the page. For instance, if a parent component has used an h2 header
40
+ * already, it will want to inform its children so that they can use h3 next, and they inform
41
+ * their children that h4 is next, and so on. (Header level tracking is actually required in
42
+ * dosgato CMS.)
43
+ *
44
+ * This function may return a promise in case you need to do something asynchronous based on
45
+ * the context received from the parent, but use it sparingly since it will stall the process.
46
+ * Try to do all asynchronous work in the fetch phase.
47
+ */
48
+ setContext(renderCtxFromParent: RenderContextType, editMode: boolean): RenderContextType | Promise<RenderContextType>;
49
+ /**
50
+ * The final phase of rendering a component is the render phase. This step is BOTTOM-UP -
51
+ * components at the bottom of the hierarchy will be rendered first, and the result of the
52
+ * render will be passed to parent components so that the HTML can be included during the
53
+ * render of the parent component.
54
+ */
55
+ abstract render(renderedAreas: Map<string, string[]>, editMode: boolean): string;
56
+ /**
57
+ * Sometimes pages are requested with an alternate extension like .rss or .ics. In these
58
+ * situations, each component should consider whether it should output anything. For
59
+ * instance, if the extension is .rss and a component represents an article, it should
60
+ * probably output an RSS item. If you don't recognize the extension, just return
61
+ * super.renderVariation(extension, renderedAreas) to give your child components a chance to
62
+ * respond, or return empty string if you want your child components to be silent in all
63
+ * cases.
64
+ *
65
+ * This function will be run after the fetch phase. The context and html rendering phases
66
+ * will be skipped.
67
+ */
68
+ renderVariation(extension: string, renderedAreas: Map<string, string>): string;
69
+ constructor(data: DataType, path: string, parent: Component | undefined);
70
+ /**
71
+ * For logging errors during rendering without crashing the render. If your fetch, setContext,
72
+ * render, or renderVariation functions throw, the error will be logged but the page render will
73
+ * continue. You generally do not need to use this function, just throw when appropriate.
74
+ */
75
+ logError(e: Error): void;
76
+ protected passError(e: Error, path: string): void;
77
+ /**
78
+ * During rendering, each component should determine the CSS blocks that it needs. This may
79
+ * change depending on the data. For instance, if you need some CSS to style up an image, but
80
+ * only when the editor uploaded an image, you can check whether the image is present during
81
+ * the execution of this function.
82
+ *
83
+ * This is evaluated after the fetch and context phases but before the rendering phase. If you
84
+ * need any async data to make this determination, be sure to fetch it during the fetch phase.
85
+ */
86
+ cssBlocks(): string[];
87
+ /**
88
+ * Same as cssBlocks() but for javascript.
89
+ */
90
+ jsBlocks(): string[];
91
+ }
92
+ export interface PageRecord<DataType extends PageData = PageData> {
93
+ id: string;
94
+ linkId: string;
95
+ path: string;
96
+ data: DataType;
97
+ }
98
+ export interface PageWithAncestors<DataType extends PageData = PageData> extends PageRecord<DataType> {
99
+ ancestors: PageRecord<PageData>[];
100
+ }
101
+ export interface ComponentData {
102
+ templateKey: string;
103
+ areas: Record<string, ComponentData[]>;
104
+ }
105
+ export interface PageData extends ComponentData {
106
+ savedAtVersion: Date;
107
+ }
108
+ export interface ContextBase {
109
+ /**
110
+ * For accessibility, every component should consider whether it is creating headers
111
+ * using h1-h6 tags, and set the context for its children so that they will use the
112
+ * next higher number. For example, a page component might use h1 for the page title,
113
+ * in which case it should set headerLevel: 2 so that its child components will use
114
+ * h2 next. Those components in turn can increment headerLevel for their children.
115
+ *
116
+ * This way every page will have a perfect header tree and avoid complaints from WAVE.
117
+ */
118
+ headerLevel: number;
119
+ }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Component = void 0;
4
+ const page_1 = require("./page");
5
+ const provider_1 = require("./provider");
6
+ /**
7
+ * This is the primary templating class to build your templates. Subclass it and provide
8
+ * at least a render function.
9
+ *
10
+ * During rendering, it will be "hydrated" - placed into a full page structure with its
11
+ * parent and child components linked.
12
+ */
13
+ class Component extends provider_1.ResourceProvider {
14
+ // the constructor is part of the recursive hydration mechanism: constructing
15
+ // a Component will also construct/hydrate all its child components
16
+ constructor(data, path, parent) {
17
+ var _a;
18
+ super();
19
+ // properties for use during hydration, you do not have to provide these when
20
+ // building a template, but you can use them in the functions you do provide
21
+ this.areas = new Map();
22
+ this.parent = parent;
23
+ const { areas, ...ownData } = data;
24
+ this.data = ownData;
25
+ this.path = path;
26
+ this.hadError = false;
27
+ let tmpParent = (_a = this.parent) !== null && _a !== void 0 ? _a : this;
28
+ while (!(tmpParent instanceof page_1.Page) && tmpParent.parent)
29
+ tmpParent = tmpParent.parent;
30
+ if (!(tmpParent instanceof page_1.Page))
31
+ throw new Error('Hydration failed, could not map component back to its page.');
32
+ this.page = tmpParent;
33
+ }
34
+ /**
35
+ * The first phase of rendering a component is the fetch phase. Each component may
36
+ * provide a fetch method that looks up data it needs from external sources. This step
37
+ * is FLAT - it will be executed concurrently for all the components on the page for
38
+ * maximum speed.
39
+ *
40
+ * Note that this.page will be available, along with its ancestors property containing
41
+ * all the data from ancestor pages, in case there is a need for inheritance. It is
42
+ * recommended to copy any needed data into the return object, as future phases will not
43
+ * want to resolve the inheritance again.
44
+ */
45
+ async fetch(editMode) {
46
+ return undefined;
47
+ }
48
+ /**
49
+ * The second phase of rendering a component is the context phase. This step is TOP-DOWN,
50
+ * each component will receive the parent component's context, modify it as desired,
51
+ * and then pass context to its children.
52
+ *
53
+ * This is useful for rendering logic that is sensitive to where the component exists in
54
+ * the hierarchy of the page. For instance, if a parent component has used an h2 header
55
+ * already, it will want to inform its children so that they can use h3 next, and they inform
56
+ * their children that h4 is next, and so on. (Header level tracking is actually required in
57
+ * dosgato CMS.)
58
+ *
59
+ * This function may return a promise in case you need to do something asynchronous based on
60
+ * the context received from the parent, but use it sparingly since it will stall the process.
61
+ * Try to do all asynchronous work in the fetch phase.
62
+ */
63
+ setContext(renderCtxFromParent, editMode) {
64
+ return renderCtxFromParent;
65
+ }
66
+ /**
67
+ * Sometimes pages are requested with an alternate extension like .rss or .ics. In these
68
+ * situations, each component should consider whether it should output anything. For
69
+ * instance, if the extension is .rss and a component represents an article, it should
70
+ * probably output an RSS item. If you don't recognize the extension, just return
71
+ * super.renderVariation(extension, renderedAreas) to give your child components a chance to
72
+ * respond, or return empty string if you want your child components to be silent in all
73
+ * cases.
74
+ *
75
+ * This function will be run after the fetch phase. The context and html rendering phases
76
+ * will be skipped.
77
+ */
78
+ renderVariation(extension, renderedAreas) {
79
+ return Array.from(renderedAreas.values()).join('');
80
+ }
81
+ /**
82
+ * For logging errors during rendering without crashing the render. If your fetch, setContext,
83
+ * render, or renderVariation functions throw, the error will be logged but the page render will
84
+ * continue. You generally do not need to use this function, just throw when appropriate.
85
+ */
86
+ logError(e) {
87
+ var _a;
88
+ this.hadError = true;
89
+ (_a = this.parent) === null || _a === void 0 ? void 0 : _a.passError(e, this.path);
90
+ }
91
+ // helper function for recursively passing the error up until it reaches the page
92
+ passError(e, path) {
93
+ var _a;
94
+ (_a = this.parent) === null || _a === void 0 ? void 0 : _a.passError(e, path);
95
+ }
96
+ /**
97
+ * During rendering, each component should determine the CSS blocks that it needs. This may
98
+ * change depending on the data. For instance, if you need some CSS to style up an image, but
99
+ * only when the editor uploaded an image, you can check whether the image is present during
100
+ * the execution of this function.
101
+ *
102
+ * This is evaluated after the fetch and context phases but before the rendering phase. If you
103
+ * need any async data to make this determination, be sure to fetch it during the fetch phase.
104
+ */
105
+ cssBlocks() {
106
+ return this.constructor.cssBlocks().keys();
107
+ }
108
+ /**
109
+ * Same as cssBlocks() but for javascript.
110
+ */
111
+ jsBlocks() {
112
+ return this.constructor.jsBlocks().keys();
113
+ }
114
+ }
115
+ exports.Component = Component;
@@ -0,0 +1,5 @@
1
+ export * from './apitemplate';
2
+ export * from './component';
3
+ export * from './links';
4
+ export * from './page';
5
+ export * from './provider';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./apitemplate"), exports);
18
+ __exportStar(require("./component"), exports);
19
+ __exportStar(require("./links"), exports);
20
+ __exportStar(require("./page"), exports);
21
+ __exportStar(require("./provider"), exports);
@@ -4,36 +4,33 @@
4
4
  * the link target anyway.
5
5
  */
6
6
  export interface AssetLink {
7
- type: 'asset'
8
- source: string
9
- id: string // the asset's dataId
10
- siteId: string
11
- path: string
12
- checksum: string
7
+ type: 'asset';
8
+ source: string;
9
+ id: string;
10
+ siteId: string;
11
+ path: string;
12
+ checksum: string;
13
13
  }
14
-
15
14
  /**
16
15
  * Some components (e.g. document list) can point at a folder instead of individual
17
16
  * assets, so we will want to track asset folders through moves, renames, and copies.
18
17
  * This link format supports all that.
19
18
  */
20
19
  export interface AssetFolderLink {
21
- type: 'assetfolder'
22
- id: string // the asset folder's guid
23
- siteId: string
24
- path: string
20
+ type: 'assetfolder';
21
+ id: string;
22
+ siteId: string;
23
+ path: string;
25
24
  }
26
-
27
25
  /**
28
26
  * A page link always points at the same pagetree as the page the link is on.
29
27
  */
30
28
  export interface PageLink {
31
- type: 'page'
32
- linkId: string
33
- siteId: string
34
- path: string
29
+ type: 'page';
30
+ linkId: string;
31
+ siteId: string;
32
+ path: string;
35
33
  }
36
-
37
34
  /**
38
35
  * The link format for external webpages. This format seems a little extra since
39
36
  * it's just a URL string. Why does it need to be an object with a type? However,
@@ -42,31 +39,28 @@ export interface PageLink {
42
39
  * the data a lot easier.
43
40
  */
44
41
  export interface WebLink {
45
- type: 'url'
46
- url: string
42
+ type: 'url';
43
+ url: string;
47
44
  }
48
-
49
45
  /**
50
46
  * Many components will point at data records. That's the whole idea. Site id is
51
47
  * required for all data links, it just might be null when the data being pointed at is
52
48
  * global data.
53
49
  */
54
50
  export interface DataLink {
55
- type: 'data'
56
- id: string // the data item's dataId
57
- siteId: string|null // null if global data
58
- path: string
51
+ type: 'data';
52
+ id: string;
53
+ siteId: string | null;
54
+ path: string;
59
55
  }
60
-
61
56
  /**
62
57
  * Just like with asset folders, we may have components that point at data folders. We
63
58
  * would like to keep the links working through moves, renames, and copies.
64
59
  */
65
60
  export interface DataFolderLink {
66
- type: 'datafolder'
67
- id: string // the asset folder's guid
68
- siteId?: string // null if global data
69
- path: string
61
+ type: 'datafolder';
62
+ id: string;
63
+ siteId?: string;
64
+ path: string;
70
65
  }
71
-
72
- export type LinkDefinition = AssetLink | AssetFolderLink | PageLink | WebLink | DataLink | DataFolderLink
66
+ export declare type LinkDefinition = AssetLink | AssetFolderLink | PageLink | WebLink | DataLink | DataFolderLink;
package/dist/links.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/page.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { PageData, ContextBase, Component, PageRecord, PageWithAncestors } from './component';
2
+ export declare abstract class Page<DataType extends PageData = any, FetchedType = any, RenderContextType extends ContextBase = any> extends Component<DataType, FetchedType, RenderContextType> {
3
+ pagePath: string;
4
+ ancestors: PageRecord[];
5
+ /**
6
+ * we will fill this before rendering, stuff that dosgato knows needs to be added to
7
+ * the <head> element
8
+ * the page's render function must include it
9
+ */
10
+ headContent: string;
11
+ protected passError(e: Error, path: string): void;
12
+ constructor(page: PageWithAncestors<DataType>);
13
+ }
package/dist/page.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Page = void 0;
4
+ const component_1 = require("./component");
5
+ class Page extends component_1.Component {
6
+ constructor(page) {
7
+ super(page.data, '/', undefined);
8
+ this.pagePath = page.path;
9
+ this.ancestors = page.ancestors;
10
+ }
11
+ passError(e, path) {
12
+ console.warn(`Recoverable issue occured during render of ${this.pagePath}. Component at ${path} threw the following error:`, e);
13
+ }
14
+ }
15
+ exports.Page = Page;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * This class is a parent class for Component, but it can also be used as a standalone
3
+ * if you are creating a set of templates with shared resources. This will be fairly
4
+ * typical for each entity running an instance and creating their own local templates. You'll
5
+ * probably want one place to set up very common resources like fontawesome or jquery, instead
6
+ * of having each and every component template provide them again and again.
7
+ *
8
+ * If you do this, don't forget to register the provider along with your templates!
9
+ */
10
+ export declare abstract class ResourceProvider {
11
+ /**
12
+ * Each template should provide a map of CSS blocks where the map key is the unique name for
13
+ * the CSS and the value is the CSS itself. For instance, if a template needs CSS from a
14
+ * standard library like jquery-ui, it could include the full CSS for jquery-ui with 'jquery-ui'
15
+ * as the key. Other templates that depend on jquery-ui would also provide the CSS, but
16
+ * a page with both components would only include the CSS once, because they both called it
17
+ * 'jquery-ui'.
18
+ *
19
+ * A version string (e.g. '1.2.5') may be provided for each block. The block with the highest
20
+ * version number of any given name will be used. Other versions of that name will be ignored.
21
+ *
22
+ * For convenience you can either provide the `css` property with the CSS as a string, or the
23
+ * `path` property with the full server path to a CSS file (node's __dirname function will
24
+ * help you determine it). You MUST provide one or the other.
25
+ */
26
+ static cssBlocks: Map<string, {
27
+ css?: string;
28
+ path?: string;
29
+ version?: string;
30
+ }>;
31
+ /**
32
+ * Same as cssBlocks() but for javascript.
33
+ */
34
+ static jsBlocks: Map<string, {
35
+ js?: string;
36
+ path?: string;
37
+ version?: string;
38
+ }>;
39
+ /**
40
+ * If your template needs to serve any files, like fonts or images, you can provide
41
+ * a filesystem path in this static property and the files will be served by the rendering
42
+ * server. Use the provided `webpaths` map to obtain the proper resource URLs. They will be
43
+ * available as soon as your template has been registered to the rendering server's templateRegistry.
44
+ *
45
+ * Typically you will set this to something like `${__dirname}/static` so that the path will be relative
46
+ * to where you are writing your template class.
47
+ *
48
+ * The map name you pick should be globally unique and only collide with other templates as
49
+ * intended. For instance, the fontawesome font only needs to be provided once, even though
50
+ * several templates might depend on it. Setting the name as 'fontawesome5' on all three
51
+ * templates would ensure that the file would only be served once. Including the major version
52
+ * number is a good idea only if the major versions can coexist.
53
+ *
54
+ * Include a version number if applicable for the file you've included with your source. If
55
+ * multiple templates have a common file, the one that provides the highest version number will
56
+ * have its file served, while the others will be ignored.
57
+ *
58
+ * DO NOT change the mime type without changing the name. Other templates could end up with
59
+ * the wrong file extension.
60
+ */
61
+ static files: Map<string, {
62
+ path: string;
63
+ version?: string;
64
+ mime: string;
65
+ }>;
66
+ /**
67
+ * Template code will need to generate HTML and CSS that points at the static files
68
+ * provided above. In order to do so, we need information from the template registry (since
69
+ * we have to deduplicate with other registered templates at startup time).
70
+ *
71
+ * In order to avoid an ES6 dependency on the registry, we will have the registry write
72
+ * back to this map as templates are registered.
73
+ *
74
+ * Now when a template needs a web path to a resource to put into its HTML, it can do
75
+ * `<img src="${TemplateClass.webpath('keyname')}">`
76
+ */
77
+ static webpaths: Map<string, string>;
78
+ static webpath(name: string): string | undefined;
79
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-extraneous-class */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ResourceProvider = void 0;
5
+ /**
6
+ * This class is a parent class for Component, but it can also be used as a standalone
7
+ * if you are creating a set of templates with shared resources. This will be fairly
8
+ * typical for each entity running an instance and creating their own local templates. You'll
9
+ * probably want one place to set up very common resources like fontawesome or jquery, instead
10
+ * of having each and every component template provide them again and again.
11
+ *
12
+ * If you do this, don't forget to register the provider along with your templates!
13
+ */
14
+ class ResourceProvider {
15
+ static webpath(name) { return this.webpaths.get(name); }
16
+ }
17
+ exports.ResourceProvider = ResourceProvider;
18
+ /**
19
+ * Each template should provide a map of CSS blocks where the map key is the unique name for
20
+ * the CSS and the value is the CSS itself. For instance, if a template needs CSS from a
21
+ * standard library like jquery-ui, it could include the full CSS for jquery-ui with 'jquery-ui'
22
+ * as the key. Other templates that depend on jquery-ui would also provide the CSS, but
23
+ * a page with both components would only include the CSS once, because they both called it
24
+ * 'jquery-ui'.
25
+ *
26
+ * A version string (e.g. '1.2.5') may be provided for each block. The block with the highest
27
+ * version number of any given name will be used. Other versions of that name will be ignored.
28
+ *
29
+ * For convenience you can either provide the `css` property with the CSS as a string, or the
30
+ * `path` property with the full server path to a CSS file (node's __dirname function will
31
+ * help you determine it). You MUST provide one or the other.
32
+ */
33
+ ResourceProvider.cssBlocks = new Map();
34
+ /**
35
+ * Same as cssBlocks() but for javascript.
36
+ */
37
+ ResourceProvider.jsBlocks = new Map();
38
+ /**
39
+ * If your template needs to serve any files, like fonts or images, you can provide
40
+ * a filesystem path in this static property and the files will be served by the rendering
41
+ * server. Use the provided `webpaths` map to obtain the proper resource URLs. They will be
42
+ * available as soon as your template has been registered to the rendering server's templateRegistry.
43
+ *
44
+ * Typically you will set this to something like `${__dirname}/static` so that the path will be relative
45
+ * to where you are writing your template class.
46
+ *
47
+ * The map name you pick should be globally unique and only collide with other templates as
48
+ * intended. For instance, the fontawesome font only needs to be provided once, even though
49
+ * several templates might depend on it. Setting the name as 'fontawesome5' on all three
50
+ * templates would ensure that the file would only be served once. Including the major version
51
+ * number is a good idea only if the major versions can coexist.
52
+ *
53
+ * Include a version number if applicable for the file you've included with your source. If
54
+ * multiple templates have a common file, the one that provides the highest version number will
55
+ * have its file served, while the others will be ignored.
56
+ *
57
+ * DO NOT change the mime type without changing the name. Other templates could end up with
58
+ * the wrong file extension.
59
+ */
60
+ ResourceProvider.files = new Map();
61
+ /**
62
+ * Template code will need to generate HTML and CSS that points at the static files
63
+ * provided above. In order to do so, we need information from the template registry (since
64
+ * we have to deduplicate with other registered templates at startup time).
65
+ *
66
+ * In order to avoid an ES6 dependency on the registry, we will have the registry write
67
+ * back to this map as templates are registered.
68
+ *
69
+ * Now when a template needs a web path to a resource to put into its HTML, it can do
70
+ * `<img src="${TemplateClass.webpath('keyname')}">`
71
+ */
72
+ ResourceProvider.webpaths = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosgato/templating",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "A library to support building templates for dosgato CMS.",
5
5
  "exports": {
6
6
  "require": "./dist/index.js",
@@ -30,5 +30,8 @@
30
30
  "bugs": {
31
31
  "url": "https://github.com/txstate-etc/dosgato-templating/issues"
32
32
  },
33
- "homepage": "https://github.com/txstate-etc/dosgato-templating#readme"
33
+ "homepage": "https://github.com/txstate-etc/dosgato-templating#readme",
34
+ "files": [
35
+ "dist", "dist-esm"
36
+ ]
34
37
  }
package/.eslintrc.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "extends": "standard-with-typescript",
3
- "parserOptions": {
4
- "project": "./tsconfig.eslint.json"
5
- },
6
- "rules": {
7
- "@typescript-eslint/explicit-function-return-type": "off", // useless boilerplate
8
- "@typescript-eslint/array-type": "off", // forces you to use Array<CustomType> instead of CustomType[], silly
9
- "@typescript-eslint/no-non-null-assertion": "off", // disables using ! to mark something as non-null,
10
- // generally not avoidable without wasting cpu cycles on a check
11
- "@typescript-eslint/no-unused-vars": "off", // typescript already reports this and VSCode darkens the variable
12
- "@typescript-eslint/return-await": ["error", "always"], // allows you to accidentally break async stacktraces in node 14+
13
- "@typescript-eslint/strict-boolean-expressions": "off" // we know how truthiness works, annoying to have to avoid
14
- }
15
- }
package/src/component.ts DELETED
@@ -1,167 +0,0 @@
1
- import { Page } from './page'
2
- import { ResourceProvider } from './provider'
3
-
4
- /**
5
- * This is the primary templating class to build your templates. Subclass it and provide
6
- * at least a render function.
7
- *
8
- * During rendering, it will be "hydrated" - placed into a full page structure with its
9
- * parent and child components linked.
10
- */
11
- export abstract class Component<DataType extends ComponentData = any, FetchedType = any, RenderContextType extends ContextBase = any> extends ResourceProvider {
12
- // properties each template should provide
13
- static templateKey: string
14
- static templateName: string
15
-
16
- // properties for use during hydration, you do not have to provide these when
17
- // building a template, but you can use them in the functions you do provide
18
- areas = new Map<string, Component[]>()
19
- data: Omit<DataType, 'areas'>
20
- fetched!: FetchedType
21
- renderCtx!: RenderContextType
22
- path: string
23
- parent?: Component
24
- page?: Page
25
- hadError: boolean
26
-
27
- /**
28
- * The first phase of rendering a component is the fetch phase. Each component may
29
- * provide a fetch method that looks up data it needs from external sources. This step
30
- * is FLAT - it will be executed concurrently for all the components on the page for
31
- * maximum speed.
32
- *
33
- * Note that this.page will be available, along with its ancestors property containing
34
- * all the data from ancestor pages, in case there is a need for inheritance. It is
35
- * recommended to copy any needed data into the return object, as future phases will not
36
- * want to resolve the inheritance again.
37
- */
38
- async fetch (editMode: boolean) {
39
- return undefined as unknown as FetchedType
40
- }
41
-
42
- /**
43
- * The second phase of rendering a component is the context phase. This step is TOP-DOWN,
44
- * each component will receive the parent component's context, modify it as desired,
45
- * and then pass context to its children.
46
- *
47
- * This is useful for rendering logic that is sensitive to where the component exists in
48
- * the hierarchy of the page. For instance, if a parent component has used an h2 header
49
- * already, it will want to inform its children so that they can use h3 next, and they inform
50
- * their children that h4 is next, and so on. (Header level tracking is actually required in
51
- * dosgato CMS.)
52
- *
53
- * This function may return a promise in case you need to do something asynchronous based on
54
- * the context received from the parent, but use it sparingly since it will stall the process.
55
- * Try to do all asynchronous work in the fetch phase.
56
- */
57
- setContext (renderCtxFromParent: RenderContextType, editMode: boolean): RenderContextType|Promise<RenderContextType> {
58
- return renderCtxFromParent
59
- }
60
-
61
- /**
62
- * The final phase of rendering a component is the render phase. This step is BOTTOM-UP -
63
- * components at the bottom of the hierarchy will be rendered first, and the result of the
64
- * render will be passed to parent components so that the HTML can be included during the
65
- * render of the parent component.
66
- */
67
- abstract render (renderedAreas: Map<string, string[]>, editMode: boolean): string
68
-
69
- /**
70
- * Sometimes pages are requested with an alternate extension like .rss or .ics. In these
71
- * situations, each component should consider whether it should output anything. For
72
- * instance, if the extension is .rss and a component represents an article, it should
73
- * probably output an RSS item. If you don't recognize the extension, just return
74
- * super.renderVariation(extension, renderedAreas) to give your child components a chance to
75
- * respond, or return empty string if you want your child components to be silent in all
76
- * cases.
77
- *
78
- * This function will be run after the fetch phase. The context and html rendering phases
79
- * will be skipped.
80
- */
81
- renderVariation (extension: string, renderedAreas: Map<string, string>) {
82
- return Array.from(renderedAreas.values()).join('')
83
- }
84
-
85
- // the constructor is part of the recursive hydration mechanism: constructing
86
- // a Component will also construct/hydrate all its child components
87
- constructor (data: DataType, path: string, parent: Component|undefined) {
88
- super()
89
- this.parent = parent
90
- const { areas, ...ownData } = data
91
- this.data = ownData
92
- this.path = path
93
- this.hadError = false
94
- let tmpParent = this.parent ?? this
95
- while (!(tmpParent instanceof Page) && tmpParent.parent) tmpParent = tmpParent.parent
96
- if (!(tmpParent instanceof Page)) throw new Error('Hydration failed, could not map component back to its page.')
97
- this.page = tmpParent
98
- }
99
-
100
- /**
101
- * For logging errors during rendering without crashing the render. If your fetch, setContext,
102
- * render, or renderVariation functions throw, the error will be logged but the page render will
103
- * continue. You generally do not need to use this function, just throw when appropriate.
104
- */
105
- logError (e: Error) {
106
- this.hadError = true
107
- this.parent?.passError(e, this.path)
108
- }
109
-
110
- // helper function for recursively passing the error up until it reaches the page
111
- protected passError (e: Error, path: string) {
112
- this.parent?.passError(e, path)
113
- }
114
-
115
- /**
116
- * During rendering, each component should determine the CSS blocks that it needs. This may
117
- * change depending on the data. For instance, if you need some CSS to style up an image, but
118
- * only when the editor uploaded an image, you can check whether the image is present during
119
- * the execution of this function.
120
- *
121
- * This is evaluated after the fetch and context phases but before the rendering phase. If you
122
- * need any async data to make this determination, be sure to fetch it during the fetch phase.
123
- */
124
- cssBlocks (): string[] {
125
- return (this.constructor as any).cssBlocks().keys()
126
- }
127
-
128
- /**
129
- * Same as cssBlocks() but for javascript.
130
- */
131
- jsBlocks (): string[] {
132
- return (this.constructor as any).jsBlocks().keys()
133
- }
134
- }
135
-
136
- export interface PageRecord<DataType extends PageData = PageData> {
137
- id: string
138
- linkId: string
139
- path: string
140
- data: DataType
141
- }
142
-
143
- export interface PageWithAncestors<DataType extends PageData = PageData> extends PageRecord<DataType> {
144
- ancestors: PageRecord<PageData>[]
145
- }
146
-
147
- export interface ComponentData {
148
- templateKey: string
149
- areas: Record<string, ComponentData[]>
150
- }
151
-
152
- export interface PageData extends ComponentData {
153
- savedAtVersion: Date
154
- }
155
-
156
- export interface ContextBase {
157
- /**
158
- * For accessibility, every component should consider whether it is creating headers
159
- * using h1-h6 tags, and set the context for its children so that they will use the
160
- * next higher number. For example, a page component might use h1 for the page title,
161
- * in which case it should set headerLevel: 2 so that its child components will use
162
- * h2 next. Those components in turn can increment headerLevel for their children.
163
- *
164
- * This way every page will have a perfect header tree and avoid complaints from WAVE.
165
- */
166
- headerLevel: number
167
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from './component'
2
- export * from './links'
3
- export * from './page'
4
- export * from './provider'
5
- export * from './template'
package/src/page.ts DELETED
@@ -1,23 +0,0 @@
1
- import { PageData, ContextBase, Component, PageRecord, PageWithAncestors } from './component'
2
-
3
- export abstract class Page<DataType extends PageData = any, FetchedType = any, RenderContextType extends ContextBase = any> extends Component<DataType, FetchedType, RenderContextType> {
4
- pagePath: string
5
- ancestors: PageRecord[]
6
-
7
- /**
8
- * we will fill this before rendering, stuff that dosgato knows needs to be added to
9
- * the <head> element
10
- * the page's render function must include it
11
- */
12
- headContent!: string
13
-
14
- protected passError (e: Error, path: string) {
15
- console.warn(`Recoverable issue occured during render of ${this.pagePath}. Component at ${path} threw the following error:`, e)
16
- }
17
-
18
- constructor (page: PageWithAncestors<DataType>) {
19
- super(page.data, '/', undefined)
20
- this.pagePath = page.path
21
- this.ancestors = page.ancestors
22
- }
23
- }
package/src/provider.ts DELETED
@@ -1,72 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-extraneous-class */
2
-
3
- /**
4
- * This class is a parent class for Component, but it can also be used as a standalone
5
- * if you are creating a set of templates with shared resources. This will be fairly
6
- * typical for each entity running an instance and creating their own local templates. You'll
7
- * probably want one place to set up very common resources like fontawesome or jquery, instead
8
- * of having each and every component template provide them again and again.
9
- *
10
- * If you do this, don't forget to register the provider along with your templates!
11
- */
12
- export abstract class ResourceProvider {
13
- /**
14
- * Each template should provide a map of CSS blocks where the map key is the unique name for
15
- * the CSS and the value is the CSS itself. For instance, if a template needs CSS from a
16
- * standard library like jquery-ui, it could include the full CSS for jquery-ui with 'jquery-ui'
17
- * as the key. Other templates that depend on jquery-ui would also provide the CSS, but
18
- * a page with both components would only include the CSS once, because they both called it
19
- * 'jquery-ui'.
20
- *
21
- * A version string (e.g. '1.2.5') may be provided for each block. The block with the highest
22
- * version number of any given name will be used. Other versions of that name will be ignored.
23
- *
24
- * For convenience you can either provide the `css` property with the CSS as a string, or the
25
- * `path` property with the full server path to a CSS file (node's __dirname function will
26
- * help you determine it). You MUST provide one or the other.
27
- */
28
- static cssBlocks: Map<string, { css?: string, path?: string, version?: string }> = new Map()
29
-
30
- /**
31
- * Same as cssBlocks() but for javascript.
32
- */
33
- static jsBlocks: Map<string, { js?: string, path?: string, version?: string }> = new Map()
34
-
35
- /**
36
- * If your template needs to serve any files, like fonts or images, you can provide
37
- * a filesystem path in this static property and the files will be served by the rendering
38
- * server. Use the provided `webpaths` map to obtain the proper resource URLs. They will be
39
- * available as soon as your template has been registered to the rendering server's templateRegistry.
40
- *
41
- * Typically you will set this to something like `${__dirname}/static` so that the path will be relative
42
- * to where you are writing your template class.
43
- *
44
- * The map name you pick should be globally unique and only collide with other templates as
45
- * intended. For instance, the fontawesome font only needs to be provided once, even though
46
- * several templates might depend on it. Setting the name as 'fontawesome5' on all three
47
- * templates would ensure that the file would only be served once. Including the major version
48
- * number is a good idea only if the major versions can coexist.
49
- *
50
- * Include a version number if applicable for the file you've included with your source. If
51
- * multiple templates have a common file, the one that provides the highest version number will
52
- * have its file served, while the others will be ignored.
53
- *
54
- * DO NOT change the mime type without changing the name. Other templates could end up with
55
- * the wrong file extension.
56
- */
57
- static files: Map<string, { path: string, version?: string, mime: string }> = new Map()
58
-
59
- /**
60
- * Template code will need to generate HTML and CSS that points at the static files
61
- * provided above. In order to do so, we need information from the template registry (since
62
- * we have to deduplicate with other registered templates at startup time).
63
- *
64
- * In order to avoid an ES6 dependency on the registry, we will have the registry write
65
- * back to this map as templates are registered.
66
- *
67
- * Now when a template needs a web path to a resource to put into its HTML, it can do
68
- * `<img src="${TemplateClass.webpath('keyname')}">`
69
- */
70
- static webpaths: Map<string, string> = new Map()
71
- static webpath (name: string) { return this.webpaths.get(name) }
72
- }
package/src/template.ts DELETED
@@ -1,88 +0,0 @@
1
- import { PageWithAncestors, ComponentData } from './component'
2
- import { LinkDefinition } from './links'
3
-
4
- export type TemplateType = 'page'|'component'|'data'
5
-
6
- /**
7
- * This interface lays out the structure the API needs for each template in the system.
8
- */
9
- export interface Template {
10
- type: TemplateType
11
-
12
- /**
13
- * A unique string to globally identify this template across installations. Namespacing like
14
- * edu.txstate.RichTextEditor could be useful but no special format is required.
15
- */
16
- templateKey: string
17
-
18
- /**
19
- * Each template must declare its areas and the template keys of components that will be
20
- * permitted inside each area. The list of allowed component templates can be updated beyond
21
- * the list provided here. See templateRegistry.addAvailableComponent's comment for info on why.
22
- */
23
- areas: Record<string, string[]>
24
-
25
- /**
26
- * Each template must provide a list of migrations for upgrading the data schema over time.
27
- * Typically this will start as an empty array and migrations will be added as the template
28
- * gets refactored.
29
- */
30
- migrations: Migration[]
31
-
32
- /**
33
- * Each template must provide a function that returns links from its data so that they
34
- * can be indexed. Only fields that are links need to be returned. Links inside rich editor
35
- * text will be extracted automatically from any text returned by getFulltext (see below)
36
- */
37
- getLinks: LinkGatheringFn
38
-
39
- /**
40
- * Each template must provide the text from any text or rich editor data it possesses, so that
41
- * the text can be decomposed into words and indexed for fulltext searches. Any text returned
42
- * by this function will also be scanned for links.
43
- */
44
- getFulltext: FulltextGatheringFn
45
-
46
- /**
47
- * Each template must provide a validation function so that the API can enforce its data is
48
- * shaped properly. If there are no issues, it should return an empty object {}, otherwise it
49
- * should return an object with keys that reference the path to the error and values that
50
- * are an array of error messages pertaining to that path.
51
- *
52
- * For instance, if name is required and the user didn't provide one, you would return:
53
- * { name: ['A name is required.'] }
54
- *
55
- * This method is async so that you can do things like look in the database for conflicting
56
- * names.
57
- */
58
- validate: (data: any) => Promise<Record<string, string[]>>
59
- }
60
-
61
- /**
62
- * In dosgato CMS, the data in the database is not altered except during user activity. This
63
- * means that older records could have been saved when the schema expected by component
64
- * rendering code was different than the date it's being rendered. To handle this, each
65
- * page and component template is required to provide migrations responsible for
66
- * transforming the data to the needed schema version.
67
- *
68
- * In order to support backwards compatibility, each API client will specify the date
69
- * when the code was written, so that their assumptions about the schema will be
70
- * frozen in time. This system means that migrations need to run backward as well as forward
71
- * in time.
72
- *
73
- * The `up` method is for changing data from an older schema to a newer one. The
74
- * `down` method is for changing data back from the newer schema to the older one.
75
- * If a `down` method cannot be provided, the migration is considered to be a breaking
76
- * change and anyone asking to rewind time to before the migration will receive an error.
77
- *
78
- * Your `up` and `down` methods will be applied to components in bottom-up fashion, so you
79
- * can assume that any components inside one of your areas has already been processed.
80
- */
81
- export interface Migration {
82
- createdAt: Date
83
- up: (data: ComponentData, page: PageWithAncestors) => ComponentData|Promise<ComponentData>
84
- down: (data: ComponentData, page: PageWithAncestors) => ComponentData|Promise<ComponentData>
85
- }
86
-
87
- export type LinkGatheringFn = (data: any) => LinkDefinition[]
88
- export type FulltextGatheringFn = (data: any) => string[]
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "include": [
4
- "src/**/*.ts",
5
- "test/**/*.ts"
6
- ]
7
- }
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "es2019",
4
- "module": "commonjs",
5
- "declaration": true,
6
- "outDir": "./dist",
7
- "strict": true,
8
- "esModuleInterop": true
9
- },
10
- "include": ["src"]
11
- }