# src/view/MainExample.ts

`1<comment>/**
2 * Comment sections may contain code examples that are placed within <example> tags.
3 *
4 * `hsDocs` will interpret and execute Javascript instructions within a <`file name='script.js'`> tag.
5 * and stylesheet instructions with a <`file name='style.css'`> tag, as in following example:
6 * `
7 * <example>
8 * <file name='script.js'>
9 * m.mount(root, {
10 * view:() => m(hslayout.Layout, { columns:[],
11 * content:['first line','second line')]
12 * })
13 * });
14 * </file>
15 *
16 * <file name='style.css'>
17 * .hsLeaf {
18 * color: blue;
19 * }
20 * </file>
21 * </example>
22 *`
23 *
24 * ### Scripts
25 * Scripts are expected to mount a `mithril Vnode` on a root DOM element using `m.mount` or `m.render`.
26 * Do not use `m.route` as only a single call is allowed per web app and that is used to manage the
27 * main hsDocs site menu and navigation.
28 *
29 * hsDocs internally uses the [global `Function` object][Function] to parse and execute the script.
30 * Thus the script has access only to global objects and to objects provided
31 * as parameters in the `Function` constructor. hsDocs currently provides the following namespaces as parameters
32 * for use in the scripts:
33 * - **m**: the `Mithril` m function
34 * - **layout**: the {@link hsLayout: `hsLayout`} namespace, providing functions to layout the browser window.
35 * - **widget**: the {@link hsGraph: `hsGraph`} namespace, providing various UI widget functions.
36 * - additionally, the parameter **root** is provided as the DOM element to mount to.
37 *
38 * [Function]:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
39 *
40 * ### Styles
41 * Styles will be automatically sandboxed so they are valid only within the enclosing example tags.
42 * This is achieved by prefixing css tags with a unique exmple ID and allows multiple examples to co-exist on the same page.
43 * In the DOM, the example ID is assgned top the <example> tag.
44 * Use `$exampleID` as css tag in the `css` section of the example to refer to the <example> element,
45 * as shown below.
46 *
47 * ### Example
48 * <example>
49 * <file name='script.js'>
50 * m.mount(root, {
51 * view:() => m(hslayout.Layout, {
52 * css:'.myExample',
53 * columns:[],
54 * content:[
55 * 'third line',
56 * 'fourth line'
57 * ]
58 * })
59 * });
60 * </file>
61 * <file name='style.css'>
62 * $exampleID { height: 200px;}
63 * .hs-layout {
64 * margin:0;
65 * }
66 * .myExample {
67 * color: red;
68 * font-weight:bold;
69 * }
70 * </file>
71 * </example>
72 */</comment>
73
74<comment>/** */</comment>
75import { m } from 'hslayout';
76import { Menu, SelectorDesc } from 'hswidget';
77import * as hswidget from 'hswidget';
78import * as hslayout from 'hslayout';
79import { Layout } from 'hslayout';
80import { shortCheckSum } from 'hsutil';
81import { delay } from 'hsutil';
82import * as hsutil from 'hsutil';
83
84const modules = {
85 m: Promise.resolve(m),
86 hsutil: Promise.resolve(hsutil), //import(<comment>/* webpackChunkName: "hsutil" */</comment> 'hsutil'),
87 hslayout: Promise.resolve(hslayout), //import(<comment>/* webpackChunkName: "hslayout" */</comment> 'hslayout'),
88 hswidget: Promise.resolve(hswidget), //import(<comment>/* webpackChunkName: "hswidget" */</comment> 'hswidget'),
89 hsdatab: import(<comment>/* webpackChunkName: "hsdatab" */</comment> 'hsdatab'),
90 hsgraph: import(<comment>/* webpackChunkName: "hsgraph" */</comment> 'hsgraph')
91};
92
93<comment>/**
94 * describes an executable comment example
95 */</comment>
96interface CommentDescriptor {
97 exampleID: string; // example tag ID
98 menuID: string; // menu tag ID
99 desc: SelectorDesc; // menu items
100 attrs?: {string?:string}; // <example attr=value>
101 pages: {string?:string}; // page content for each menu item
102 executeScript?: (root:any) => void; // the example code to execute
103 executeSource?: ''; // the source code to execute
104 activeSrcPage:string; // the active page for the source display
105}
106
107<comment>/**
108 * Map containing various exampkle configurations
109 */</comment>
110const gInitialized:{string?:CommentDescriptor} = {};
111
112<comment>/**
113 * creates the example configuration, generates the DOM hook, and sets up the example execution.
114 * `example` takes a context map of the form `{ name:module, ...}` containing libraries to be used
115 * withibn the example, and returns a function that can be used in calls to `string.replace`.
116 * See the following example:
117 * `

     118 * import * as layout from "layout";

     119 * text = text.replace(/<example>([\S\s]*?)<\/example>/gi, example({layout:layout}));

     120 *

`
121 * The modules `m`, `hsLayout`, and `hsGraph` will be added by default as
122 * ` { m: m, layout: layout, widget: widget } `
123 * @param exmpl the example string extracted from the comment, including the `<example>` tags.
124 * @param context the context in which the example script should be run, expressed as `name`:`namespace` pairs.
125 */</comment>
126export function example(exmpl:string) {
127 const instance = shortCheckSum(exmpl);
128 let IDs = gInitialized[instance];
129 if (!IDs) {
130 IDs = gInitialized[instance] = initDesc(IDs);
131 IDs.executeSource = exmpl;
132 createExecuteScript(IDs, exmpl);
133 }
134 if (!document.getElementById(IDs.menuID)) {
135 addExample(IDs).then(delay(1)).then(executeScript).catch(executeError);
136 }
137
138 const frameHeight = (IDs.attrs? IDs.attrs.height : undefined) || '300px';
139 const wrapWithID = (css:string) => css==='$exampleID'? `#${IDs.exampleID}`: `#${IDs.menuID} ${css}`;
140
141 // prefix css selectors with ID of main execution area to sandbox the scope
142 // (^|}): start of string or end of previous style def
143 // \s*?: any white spaces
144 // (\S*?): capturing group: css name
145 // \s*?{: whitespaces, followed by start of style def
146 const style = (!IDs.pages['css'])? '': // empty if no css defined
147 IDs.pages['css'].replace(/(^|})\s*?(\S*?)\s*?{/gi, // otherwise wrap each css statement
148 (x:string, ...args:any[]) => x.replace(args[1], wrapWithID(args[1]))
149 );
150 return `
<example id=${IDs.exampleID} style="height:${frameHeight}" class="hs-layout-frame"></example>`;
151}
152
153function createExecuteScript(IDs:CommentDescriptor, exmpl:string): Promise<boolean> {
154 const libNames = Object.keys(modules);
155 return Promise.all(libNames.map(name => modules[name]))
156 .then((libs) => {
157 try {
158 const scriptFn = new Function('root', ...libNames, getCommentDescriptor(IDs, exmpl));
159 IDs.executeScript = (root:any) => scriptFn(root, ...libs);
160 return true;
161 }
162 catch(e) {
163 console.log('creating script:' + e);
164 return false;
165 }
166 });
167}
168
169<comment>/**
170 * creates the example configuration
171 */</comment>
172function initDesc(IDs:CommentDescriptor):CommentDescriptor {
173 return {
174 exampleID: getNewID(), // example tag ID
175 menuID: getNewID(), // main execution area tag ID
176 desc:<SelectorDesc>{
177 items:<string[]>[],
178 selectedItem: 'js',
179 changed: () => addExample(IDs) // called when source menu changes
180 .then(executeScript)
181 .catch(executeError),
182 size: ["50px"]
183 },
184 pages:{},
185 activeSrcPage: undefined
186 };
187}
188
189<comment>/** creates a new random example ID for each call. */</comment>
190function getNewID():string {
191 return 'hs'+Math.floor(1000000*Math.random());
192}
193
194<comment>/** asynchronously adds the example structure on the page and then executed the script. */</comment>
195function addExample(IDs:CommentDescriptor):Promise<CommentDescriptor> {
196 return Promise.resolve(IDs).then(addExampleStructure).then(delay(1));
197}
198
199<comment>/**
200 * returns a parameterless function that can be called via setTimeout to
201 * mount the menu and execute the script function provided in `IDs`.
202 * @param IDs the `CommentDescriptor` to execute on.
203 */</comment>
204function addExampleStructure(IDs:CommentDescriptor):CommentDescriptor {
205 let item = IDs.activeSrcPage || 'js';
206 const root = document.getElementById(IDs.exampleID);
207
208 IDs.desc.changed = (newItem:string) => {
209 item = IDs.activeSrcPage = newItem;
210 };
211
212 m.mount(root, {view: () => m(Layout, {
213 columns: ["50%"],
214 content: [
215 m(Layout, {
216 content: m('.hs-layout .hs-execution', {id:IDs.menuID}, 'placeholder')
217 }),
218 m(Layout, {
219 rows:["30px", "fill"],
220 css: '.hs-source',
221 content:[
222 m(Menu, {desc: IDs.desc, size:['50px'] }),
223 m(Layout, { content: m('.hs-layout .hs-source-main', m.trust(``

    ${IDs.pages[item]}

``))})
224 ]
225 })
226 ]})
227 });
228 return IDs;
229}
230
231<comment>/**
232 * execute the provided script
233 * @param IDs provides the context in which the script is exceuted/
234 */</comment>
235function executeScript(IDs:CommentDescriptor) {
236 const root = document.getElementById(IDs.menuID);
237 if (root) {
238 console.log(`root found for menuID ${IDs.menuID}`);
239 try { IDs.executeScript(root); }
240 catch(e) {
241 console.log("error executing script: " + e);
242 console.log(IDs.executeSource);
243 console.log(e.stack);
244 }
245 } else {
246 console.log(`root not found for menuID ${IDs.menuID}`);
247 }
248 m.redraw();
249 return IDs;
250}
251
252function executeError(e:any) {
253 console.log('rejection executing script:');
254 console.log(e);
255}
256
257<comment>/**
258 * Fills in all fields of the CommentDescriptor provided as `IDs`.
259 * @param IDs the CommentDescriptor to complete
260 * @param example the example string, including <example> tag
261 * @return the script to execute, as a string
262 */</comment>
263function getCommentDescriptor(IDs:CommentDescriptor, example:string):string {
264 let result = '';
265 let attrs = example.match(/<example\s(\S*?)(\s|>)/i);
266 if (attrs && attrs[1]) {
267 const at = attrs[1].split('=');
268 IDs.attrs = {[at[0]]: at[1]};
269 }
270 example.replace(/<file[\s]*name=[\S]*?\.([\s\S]*?)['|"]>([\S\s]*?)<\/file>/gi, function(text:string) {
271 const args = [...arguments];
272 const content = args[2].trim(); // the part between <file> and </file>
273 IDs.desc.items.push(args[1]); // record available extensions, i.e. 'js', 'html', etc
274 IDs.pages[args[1]] = content; // associate the content with the extension
275 return result;
276 });
277 return IDs.pages['js'];
278}
`