# MainExample.ts

`

1<comment>/**</comment>

2<comment> * Comment sections may contain code examples that are intended to be live demonstrations for </comment>

3<comment> * explaining some concept of the module are contained in. </comment>

4<comment> * </comment>

5<comment> * `hsDocs` will interpret comment sections between `<example>` and `</example>`</comment>

6<comment> * tags as code examples. Inside a code example, `<file name=<quote>'script.js'</quote>>` tags contain a script to be executed, </comment>

7<comment> * and `<file name=<quote>'style.css'</quote>>` tags contain stylesheet instructions to apply, as in the following snippet (also see Example below):</comment>

8<comment> * `</comment>`

9<comment> * <example> </comment>

10<comment> * <file name=<quote>'script.js'</quote>></comment>

11<comment> * m.mount(root, { </comment>

12<comment> * view:() => m(hsLayout.Layout, { columns:[], </comment>

13<comment> * content:[<quote>'first line'</quote>,<quote>'second line'</quote>)]</comment>

14<comment> * })</comment>

15<comment> * });</comment>

16<comment> * </file></comment>

17<comment> * </comment>

18<comment> * <file name=<quote>'style.css'</quote>></comment>

19<comment> * .hsLeaf { </comment>

20<comment> * color: blue; </comment>

21<comment> * }</comment>

22<comment> * </file></comment>

23<comment> * </example></comment>

24<comment> *

` </comment>

25<comment> * </comment>

26<comment> * ### Scripts </comment>

27<comment> * Scripts are enclosed by `<file name=<quote>'script.js'</quote>>` and `</file>` tags. </comment>

28<comment> * </comment>

29<comment> * hsDocs internally uses the [global Function object][Function] to parse and execute the script. </comment>

30<comment> * Thus the script runs in the global scope and has access to global objects. A number of additional modules are provided</comment>

31<comment> * as parameters in the `Function` constructor and are thus available to the script. </comment>

32<comment> * hsDocs currently provides the following default namespaces to access the corresponding modules in the scripts:</comment>

33<comment> * - **m**: namespace for the [`Mithril`](https:<comment>//mithril.js.org/) m function </comment></comment>

34<comment><comment></comment> * - **hsLayout**: namespace for the [`hsLayout`](http:<comment>//helpfulscripts.github.io/hsLayout/#!/api/hsLayout/0) library, </comment></comment>

35<comment><comment></comment> * providing functions to layout the browser window.</comment>

36<comment> * - **hsWidget**: namespace for the [`hsWidget`](https:<comment>//helpfulscripts.github.io/hsWidget/#!/api/hsWidget/0) library, </comment></comment>

37<comment><comment></comment> * providing various UI widget functions.</comment>

38<comment> * - **root**: the DOM element to attach content to.</comment>

39<comment> * </comment>

40<comment> * Additional Scripts can loaded - see `Configuring the example` below. </comment>

41<comment> * </comment>

42<comment> * The script can access and manipulate any of the provided libraries</comment>

43<comment> * via their namespace, for example as in `m(hsLayout.Layout,{})`</comment>

44<comment> * #### Using Mithril:</comment>

45<comment> * mount a `mithril Vnode` on the provided root DOM element using `m.mount` or `m.render`. </comment>

46<comment> * Do not use `m.route` as only a single call is allowed per web app and that is used to manage the </comment>

47<comment> * main hsDocs site menu and navigation.</comment>

48<comment> * ```</comment>

49<comment> * m.mount(root, { view:() => m(<quote>'div'</quote>, <quote>'content'</quote>)});</comment>

50<comment> * ```</comment>

51<comment> * #### Using d3:</comment>

52<comment> * ```</comment>

53<comment> * const base = d3.select(root);</comment>

54<comment> * const svg = base.append(<quote>'svg'</quote>)</comment>

55<comment> * .classed(<quote>'baseSVG'</quote>, true)</comment>

56<comment> * .attr(<quote>'height'</quote>, <quote>'100%'</quote>)</comment>

57<comment> * .attr(<quote>'width'</quote>, <quote>'100%'</quote>)</comment>

58<comment> * .attr(<quote>'preserveAspectRatio'</quote>, <quote>'xMinYMin meet'</quote>);</comment>

59<comment> * ```</comment>

60<comment> * </comment>

61<comment> * ### Configuring the `example`</comment>

62<comment> * The `<example>` accepts additional arguments to configure the example:</comment>

63<comment> * </comment>

64<comment> * #### Height of the `example` panel</comment>

65<comment> * To change the default panel height of 300px, set the `height=<numper>px` argument.</comment>

66<comment> * </comment>

67<comment> * #### Module injection</comment>

68<comment> * Additional module libraries can be injected by adding the argument `libs={<export name>:<path>[,...]}`.</comment>

69<comment> * `hsDocs` will try to load the library via the provided `path` and make it available to the script within the namespace `export name`. </comment>

70<comment> * For example:</comment>

71<comment> * `</comment>`

72<comment> * <example height=500px libs={hsGraphd3:<quote>'hsGraphd3'</quote>, d3:<quote>'https:<comment>//d3js.org/d3.v5.min.js'</quote>}></comment></comment>

73<comment><comment></comment> * ...in the script:</comment>

74<comment> * new hsGraph.Graph();</comment>

75<comment> * d3.slect(<quote>''</quote>);</comment>

76<comment> * </comment>

77<comment> * The following rules apply in resolving `path`:</comment>

78<comment> * 1. If `path` is structured, i.e. contains <quote>'/'</quote>, it is requested as is - see <quote>'d3'</quote> example above.</comment>

79<comment> * 2. load from `node_modules/<path.toLowerCase()>/<path>.js`, relative to the current web page. </comment>

80<comment> * This is useful to maintain libraries installed via `npm` locally on the server.</comment>

81<comment> * 3. load from `https:<comment>//helpfulscripts.github.io/<path>/<path>.js`. Provided as a convenience to access `helpfulscript` modules ;)</comment></comment>

82<comment><comment></comment> * </comment>

83<comment> * To be recognized as injectable module, libraries need to follow the convention of `webPack<quote>'s` </comment>

84<comment> * [`output.libraryTarget: <quote>"this"</quote>`](https:<comment>//webpack.js.org/configuration/output/#expose-via-object-assignment) configuration.</comment></comment>

85<comment><comment></comment> * </comment>

86<comment> * [Function]:https:<comment>//developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function</comment></comment>

87<comment><comment></comment> * </comment>

88<comment> * ### Styles</comment>

89<comment> * Styles will be automatically sandboxed so they are valid only within the enclosing example tags. </comment>

90<comment> * This is achieved by prefixing css tags with a unique exmple ID and allows multiple examples to co-exist on the same page.</comment>

91<comment> * In the DOM, the example ID is assgned top the <example> tag.</comment>

92<comment> * Use `$exampleID` as css tag in the `css` section of the example to refer to the <example> element, </comment>

93<comment> * as shown below.</comment>

94<comment> * </comment>

95<comment> * ### Example</comment>

96<comment> * <example></comment>

97<comment> * <file name='</quote>script.js<quote>'></comment>

98<comment> * m.mount(root, { </comment>

99<comment> * view:() => m(hsLayout.Layout, {</comment>

100<comment> * css:'</quote>.myExample<quote>', </comment>

101<comment> * columns:[], </comment>

102<comment> * content:[</comment>

103<comment> * '</quote>left column<quote>',</comment>

104<comment> * '</quote>right column<quote>'</comment>

105<comment> * ]</comment>

106<comment> * })</comment>

107<comment> * });</comment>

108<comment> * </file></comment>

109<comment> * <file name='</quote>style.css<quote>'></comment>

110<comment> * $exampleID { height: 200px;}</comment>

111<comment> * .hs-layout { </comment>

112<comment> * margin:0; </comment>

113<comment> * }</comment>

114<comment> * .myExample { </comment>

115<comment> * color: red; </comment>

116<comment> * font-weight:bold; </comment>

117<comment> * }</comment>

118<comment> * </file></comment>

119<comment> * </example></comment>

120<comment> */</comment>

121

122<comment>/** */</comment>

123import { m } from '</quote>hslayout<quote>';

124import { Menu, SelectorDesc } from '</quote>hswidget<quote>';

125import * as hswidget from '</quote>hswidget<quote>';

126import * as hslayout from '</quote>hslayout<quote>';

127import { Layout } from '</quote>hslayout<quote>';

128import { shortCheckSum } from '</quote>hsutil<quote>';

129import { delay } from '</quote>hsutil<quote>';

130import { Log } from '</quote>hsutil<quote>'; const log = new Log('</quote>MainExample<quote>');

131import * as hsutil from '</quote>hsutil<quote>';

132

133const modules = {

134 m: m,

135 hsUtil: hsutil,

136 hsLayout: hslayout,

137 hsWidget: hswidget

138};

139

140interface Attribute {

141 height?: string;

142 libs?: {sym:string, path:string}[];

143}

144

145<comment>/**</comment>

146<comment> * describes an executable comment example</comment>

147<comment> */</comment>

148interface CommentDescriptor {

149 created: boolean; <comment>// indicates if all dynamic modules are loaded</comment>

150<comment></comment> exampleID: string; <comment>// example tag ID</comment>

151<comment></comment> menuID: string; <comment>// menu tag ID</comment>

152<comment></comment> desc: SelectorDesc; <comment>// menu items</comment>

153<comment></comment> attrs?: Attribute; <comment>// <example attr=value></comment>

154<comment></comment> pages: {string?:string}; <comment>// page content for each menu item</comment>

155<comment></comment> runScript?: (root:any) => void; <comment>// the example code to execute</comment>

156<comment></comment> executeSource?: '</quote><quote>'; <comment>// the source code to execute</comment>

157<comment></comment> activeSrcPage:string; <comment>// the active page for the source display</comment>

158<comment></comment>}

159

160<comment>/**</comment>

161<comment> * Map containing various exampkle configurations </comment>

162<comment> */</comment>

163const gInitialized:{string?:CommentDescriptor} = {};

164

165<comment>/** used to generate unique IDs for examples. */</comment>

166let gIDCounter = 100000;

167

168<comment>/** creates a new random example ID for each call. */</comment>

169function getNewID():string {

170 <comment>// return '</quote>hs<quote>'+Math.floor(1000000*Math.random());</comment>

171<comment></comment> return '</quote>hs<quote>'+gIDCounter++;

172}

173

174

175<comment>/**</comment>

176<comment> * creates the example configuration, generates the DOM hook, and sets up the example execution.</comment>

177<comment> * `example` takes a context map of the form `{ name:module, ...}` containing libraries to be used </comment>

178<comment> * within the example, and returns a function that can be used in calls to `string.replace`. </comment>

179<comment> * See the following example:</comment>

180<comment> * ``

    </comment>
     181<comment> * import * as layout from <quote>"layout"</quote>;</comment>
     182<comment> * text = text.replace(/<example>([\S\s]*?)<\/example>/gi, example({layout:layout}));</comment>
     183<comment> *

</comment>

184<comment> * The modules `m`, `hsLayout`, and `hsGraph` will be added by default as </comment>

185<comment> * ` { m: m, layout: layout, widget: widget } `</comment>

186<comment> * @param exmpl the example string extracted from the comment, including the `<example>` tags.</comment>

187<comment> * @param context the context in which the example script should be run, expressed as `name`:`namespace` pairs.</comment>

188<comment> */</comment>

189export function example(exmpl:string) {

190 const instance = shortCheckSum(exmpl);

191 let IDs = gInitialized[instance];

192 if (!IDs) {

193 IDs = gInitialized[instance] = initDesc(IDs);

194 IDs.executeSource = exmpl;

195 createExecuteScript(IDs, exmpl);

196 }

197 if (!document.getElementById(IDs.menuID)) {

198 addExampleStructure(IDs);

199 delay(1)(IDs)

200 .then(executeScript)

201 .catch(executeError);

202 }

203

204 const frameHeight = (IDs.attrs? IDs.attrs.height : undefined) || '</quote>300px<quote>';

205 const wrapWithID = (css:string) => css==='</quote>$exampleID<quote>'? `#${IDs.exampleID}`: `#${IDs.menuID} ${css}`;

206

207 <comment>// prefix css selectors with ID of main execution area to sandbox the scope</comment>

208<comment></comment> <comment>// (^|}): start of string or end of previous style def</comment>

209<comment></comment> <comment>// \s*?: any white spaces</comment>

210<comment></comment> <comment>// (\S*?): capturing group: css name</comment>

211<comment></comment> <comment>// \s*?{: whitespaces, followed by start of style def</comment>

212<comment></comment> const style = (!IDs.pages['</quote>css<quote>'])? '</quote><quote>': <comment>// empty if no css defined</comment>

213<comment></comment> IDs.pages['</quote>css<quote>'].replace(/(^|})\s*?(\S*?)\s*?{/gi, <comment>// otherwise wrap each css statement</comment>

214<comment></comment> (x:string, ...args:any[]) => x.replace(args[1], wrapWithID(args[1]))

215 );

216 return `
<example id=${IDs.exampleID} style=<quote>"height:${frameHeight}"</quote> class=<quote>"hs-layout-frame"</quote>></example>`;

217}

218

219async function createExecuteScript(IDs:CommentDescriptor, exmpl:string): Promise<boolean> {

220 try {

221 const desc = await getCommentDescriptor(IDs, exmpl);

222 IDs.runScript = (root:any) => {

223 const libNames = Object.keys(modules);

224 const missingLibs = IDs.attrs.libs.map(l => l.sym).filter(l => libNames.indexOf(l)<0);

225 if (missingLibs.length > 0) {

226 log.info(`expected modules to be loaded: ${missingLibs.join('</quote>, <quote>')}`);

227 } else {

228 const libs = libNames.map(name => modules[name]);

229 const scriptFn = new Function('</quote>root<quote>', ...libNames, desc);

230 log.info(`running example script with modules '</quote>${libNames.join(<quote>', '</quote>)}<quote>'`);

231 scriptFn(root, ...libs);

232 }

233 };

234 log.debug('</quote>example function ready<quote>');

235 IDs.created = true;

236 return true;

237 } catch(e) {

238 log.error(`creating script: ${e}\n${e.stack}`);

239 return false;

240 }

241}

242

243<comment>/**</comment>

244<comment> * creates the example configuration </comment>

245<comment> */</comment>

246function initDesc(IDs:CommentDescriptor):CommentDescriptor {

247 return {

248 created: false,

249 exampleID: getNewID(), <comment>// example tag ID</comment>

250<comment></comment> menuID: getNewID(), <comment>// main execution area tag ID</comment>

251<comment></comment> desc:<SelectorDesc>{

252 items:<string[]>[],

253 selectedItem: '</quote>js<quote>',

254 clicked: async () => {

255 try {

256 addExampleStructure(IDs); <comment>// called when source menu changes</comment>

257<comment></comment> await delay(1)();

258 await executeScript(IDs);

259 }

260 catch(e) { executeError(e); }

261 },

262 size: [<quote>"50px"</quote>]

263 },

264 pages:{},

265 activeSrcPage: undefined,

266 attrs: {libs:[]}

267 };

268}

269

270<comment>/**</comment>

271<comment> * returns a parameterless function that can be called via setTimeout to </comment>

272<comment> * mount the menu and execute the script function provided in `IDs`. </comment>

273<comment> * @param IDs the `CommentDescriptor` to execute on. </comment>

274<comment> */</comment>

275function addExampleStructure(IDs:CommentDescriptor):CommentDescriptor {

276 let item = IDs.activeSrcPage || '</quote>js<quote>';

277 const root = document.getElementById(IDs.exampleID);

278

279 IDs.desc.clicked = (newItem:string) => {

280 item = IDs.activeSrcPage = newItem;

281 };

282

283 if (root && IDs.created) {

284 m.mount(root, {view: () => m(Layout, {

285 columns: [<quote>"50%"</quote>],

286 content: [

287 m(Layout, {

288 content: m('</quote>.hs-layout .hs-execution<quote>', {id:IDs.menuID}, m('</quote>div.placeholder<quote>', '</quote>placeholder<quote>'))

289 }),

290 m(Layout, {

291 rows:[<quote>"30px"</quote>, <quote>"fill"</quote>],

292 css: '</quote>.hs-source<quote>',

293 content:[

294 m(Menu, {desc: IDs.desc, size:['</quote>50px<quote>'] }),

295 m(Layout, { content: m('</quote>.hs-layout .hsdocs_source_main<quote>', m.trust(```

    ${IDs.pages[item]}

`))})

296 ]

297 })

298 ]})

299 });

300 }

301 return IDs;

302}

303

304<comment>/**</comment>

305<comment> * execute the provided script </comment>

306<comment> * @param IDs provides the context in which the script is exceuted/</comment>

307<comment> */</comment>

308function executeScript(IDs:CommentDescriptor) {

309 const root = document.getElementById(IDs.menuID);

310 if (root && IDs.created) {

311 <comment>// root.querySelectorAll('</quote>.placeholder<quote>').forEach(d => root.removeChild(d));</comment>

312<comment></comment> while (root.children.length>0) { root.removeChild(root.children[0]); }

313 try { IDs.runScript(root); }

314 catch(e) {

315 log.error(`executing script: ${e}\n${IDs.executeSource}\n${e.stack}`);

316 }

317 } else {

318 <comment>// log.error(`root not found for menuID ${IDs.menuID}`);</comment>

319<comment></comment> }

320 m.redraw();

321 return IDs;

322}

323

324function executeError(e:any) {

325 log.error(`rejection executing script:\n${e}\n${e.stack}`);

326}

327

328<comment>/**</comment>

329<comment> * Fills in all fields of the CommentDescriptor provided as `IDs`.</comment>

330<comment> * @param IDs the CommentDescriptor to complete</comment>

331<comment> * @param example the example string, including <example> tag</comment>

332<comment> * @return the script to execute, as a string</comment>

333<comment> */</comment>

334async function getCommentDescriptor(IDs:CommentDescriptor, example:string):Promise<string> {

335 let result = '</quote><quote>';

336

337 <comment>// let attrs = example.match(/<example\s(\S*?)(\s|>)/gi);</comment>

338<comment></comment> let attrs = example.match(/<example\s([\s\S]*?)>/); <comment>// capture all parameters to `example`</comment>

339<comment></comment> if (attrs && attrs[1]) {

340 findTokens(IDs, attrs[1]);

341 if (IDs.attrs.libs.length>0) {

342 await Promise.all(IDs.attrs.libs.map(async l => {

343 log.debug(`loading lib: ${l.sym}: ${l.path}`);

344 const content = await loadScript(l.sym, l.path);

345 if (content) { modules[l.sym] = content[l.sym]; }

346 }));

347 }

348 log.debug(`found attrs:\n ${log.inspect(IDs.attrs, 5)}`);

349 }

350 example.replace(/<file[\s]*name=[\S]*?\.([\s\S]*?)['</quote>|<quote>"]>([\S\s]*?)<\/file>/gi, function(text:string) {

351 const args = [...arguments];

352 const content = args[2].trim(); <comment>// the part between <file> and </file></comment>

353<comment></comment> IDs.desc.items.push(args[1]); <comment>// record available extensions, i.e. <quote>'js'</quote>, <quote>'html'</quote>, etc </comment>

354<comment></comment> IDs.pages[args[1]] = content.trim(); <comment>// associate the content with the extension</comment>

355<comment></comment> return result;

356 });

357 return IDs.pages[<quote>'js'</quote>];

358}

359

360function findTokens(IDs:CommentDescriptor, str:string) {

361 const results = [];

362 <comment>// find <quote>'key=value'</quote> and <quote>'key={value}'</quote> statements</comment>

363<comment></comment> const reg = new RegExp(/((\w+?)\s*?=\s*?([^\s{},]+|{[\s\S]+?}))/g);

364 let r;

365 while(r = reg.exec(str)) {

366 results.push({key:r[2], val:r[3]});

367 }

368 results.map(kv => {

369 switch (kv.key) {

370 case <quote>'height'</quote>: IDs.attrs[<quote>'height'</quote>] = kv.val; break;

371 case <quote>'libs'</quote>: <comment>// {graph:<quote>'/node_modules/hsgraphd3/index.js'</quote>}</comment>

372<comment></comment> const ligreg = new RegExp(/((\w+?)\s*:\s*([^\s,}]+))/g);

373 while(r = ligreg.exec(kv.val)) {

374 IDs.attrs.libs.push({sym:r[2],path:r[3].replace(/[<quote>'"</quote>]/g, '</quote><quote>')});

375 }

376 }

377 });

378 log.debug(`${str} --> \n${log.inspect(IDs.attrs,5)}`);

379}

380

381

382<comment>/**</comment>

383<comment> * attempts to load a library for the specified `path`. If `path` is a module nane (e.g. '</quote>hsDocs<quote>'), </comment>

384<comment> * this involves the following attempts</comment>

385<comment> * at resolving the library path:</comment>

386<comment> * 1. load from '</quote>node_modules/<path.toLowerCase()>/<path>.js, relative to the current web page.</comment>

387<comment> * 2. load from <quote>'https:<comment>//helpfulscripts.github.io/<path>/<path>.js'</quote></comment></comment>

388<comment><comment></comment> * If `path` is structured, i.e. contains <quote>'/'</quote>, it is requested as is. </comment>

389<comment> * @param path module name, such as <quote>'hsDocs'</quote></comment>

390<comment> */</comment>

391async function loadScript(sym:string, path:string) {

392 const load = (p:string) => m.request({ method: <quote>"GET"</quote>, url: p, extract: (xhr:any) => xhr });

393

394 const paths = [

395 `node_modules/${path.toLowerCase()}/${path}.js`,

396 `https:<comment>//helpfulscripts.github.io/${path}/${path}.js`</comment>

397<comment></comment> ];

398 let content:any;

399 try {

400 if (path.indexOf(<quote>'/'</quote>)>=0) { <comment>// if structured: call as is</comment>

401<comment></comment> content = await load(path);

402 } else { <comment>// else try other paths</comment>

403<comment></comment> for (let i=0; i<paths.length; i++) {

404 try {

405 path = paths[i];

406 content = await load(paths[i]);

407 if (content.status === 200) { break; }

408 }

409 catch(e) {}

410 }

411 <comment>// log.info(`${path} of ${paths.join(<quote>', '</quote>)} ${content?<quote>'found'</quote>:<quote>'not found'</quote>}`);</comment>

412<comment></comment> }

413 } catch(e) { log.error(`loading lib ${path}`);}

414 let code:string;

415 try {

416 code = content.responseText;

417 let lib:any = {};

418 log.info(`loaded ${path}, creating library`);

419 new Function(code).bind(lib)();

420 if (lib) {

421 return lib;

422 } else {

423 log.warn(`wrong lib format for ${sym}: ${code.slice(0, 20)}..., should be 'this[<quote>"${sym}"</quote>] = ...`);

424 }

425 } catch(e) {

426 log.error(`JSON.parse: ${path}: ${e}`);

427 log.error(code);

428 return undefined;

429 }

430 <comment>// add(content.responseText);</comment>

431<comment></comment>}

432

433