diff --git a/.vscode/settings.json b/.vscode/settings.json index 945952f74..2c87465b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "cSpell.words": ["Scully", "Scullyio"], + "cSpell.words": ["Scully", "Scullyio", "ngcontent"], "peacock.affectActivityBar": true, "peacock.affectTitleBar": true, "peacock.affectStatusBar": true, diff --git a/projects/sampleBlog/src/app/blog/blog-list/blog-list.component.ts b/projects/sampleBlog/src/app/blog/blog-list/blog-list.component.ts index 15e327490..d65fbc505 100644 --- a/projects/sampleBlog/src/app/blog/blog-list/blog-list.component.ts +++ b/projects/sampleBlog/src/app/blog/blog-list/blog-list.component.ts @@ -9,7 +9,8 @@ import {map} from 'rxjs/operators'; }) export class BlogListComponent implements OnInit { blogs$ = this.srs.available$.pipe( - map(routeList => routeList.filter((route: ScullyRoute) => route.route.startsWith(`/blog/`))) + map(routeList => routeList.filter((route: ScullyRoute) => route.route.startsWith(`/blog/`))), + map(blogs => blogs.sort((a, b) => (a.date < b.date ? -1 : 1))) ); constructor(private srs: ScullyRoutesService) {} diff --git a/projects/sampleBlog/src/app/blog/blog.component.css b/projects/sampleBlog/src/app/blog/blog.component.css index aadded8e9..eff20d6d6 100644 --- a/projects/sampleBlog/src/app/blog/blog.component.css +++ b/projects/sampleBlog/src/app/blog/blog.component.css @@ -1,4 +1,4 @@ -::slotted(h1) { +h1 { color: rgb(51, 6, 37); background-color: rgb(248, 211, 236); padding: 5px; diff --git a/projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.ts b/projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.ts index 82abb9739..fb12b7488 100644 --- a/projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.ts +++ b/projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.ts @@ -2,18 +2,26 @@ import { ChangeDetectionStrategy, Component, ElementRef, - Input, OnDestroy, OnInit, ViewEncapsulation, } from '@angular/core'; import {Router} from '@angular/router'; -import {Observable, Subscription} from 'rxjs'; import {take} from 'rxjs/operators'; import {IdleMonitorService} from '../idleMonitor/idle-monitor.service'; import {ScullyRoutesService} from '../route-service/scully-routes.service'; import {fetchHttp} from '../utils/fetchHttp'; +import {findComments} from '../utils/findComments'; +interface ScullyContent { + html: string; + cssId: string; +} +declare global { + interface Window { + scullyContent: ScullyContent; + } +} /** this is needed, because otherwise the CLI borks while building */ const scullyBegin = ''; const scullyEnd = ''; @@ -36,22 +44,15 @@ const scullyEnd = ''; preserveWhitespaces: true, }) export class ScullyContentComponent implements OnInit, OnDestroy { - @Input() type = 'MD'; - elm = this.elmRef.nativeElement as HTMLElement; - // mutationSubscription: Subscription; + /** pull in all available routes into an eager promise */ routes = this.srs.available$.pipe(take(1)).toPromise(); - constructor( - private elmRef: ElementRef, - private srs: ScullyRoutesService, - private router: Router, - private idle: IdleMonitorService - ) {} + constructor(private elmRef: ElementRef, private srs: ScullyRoutesService, private router: Router) {} ngOnInit() { - /** make sure the idle-check is loaded. */ - this.idle.init(); + // /** make sure the idle-check is loaded. */ + // this.idle.init(); if (this.elm) { /** this will only fire in a browser environment */ this.handlePage(); @@ -64,11 +65,16 @@ export class ScullyContentComponent implements OnInit, OnDestroy { */ private async handlePage() { const template = document.createElement('template'); - // tslint:disable-next-line: no-string-literal - if (window['scullyContent']) { + const currentCssId = this.getCSSId(this.elm); + if (window.scullyContent) { /** upgrade existing static content */ - // tslint:disable-next-line: no-string-literal - template.innerHTML = window['scullyContent']; + const htmlString = window.scullyContent.html; + if (currentCssId !== window.scullyContent.cssId) { + /** replace the angular cssId */ + template.innerHTML = htmlString.split(window.scullyContent.cssId).join(currentCssId); + } else { + template.innerHTML = htmlString; + } } else { const curPage = location.href; /** @@ -81,7 +87,12 @@ export class ScullyContentComponent implements OnInit, OnDestroy { await fetchHttp(curPage, 'text') .then((html: string) => { try { - template.innerHTML = html.split(scullyBegin)[1].split(scullyEnd)[0]; + const htmlString = html.split(scullyBegin)[1].split(scullyEnd)[0]; + if (htmlString.includes('_ngcontent')) { + /** update the angular cssId */ + const atr = '_ngcontent' + htmlString.split('_ngcontent')[1].split('=')[0]; + template.innerHTML = htmlString.split(atr).join(currentCssId); + } } catch (e) { template.innerHTML = `

Sorry, could not parse static page content

This might happen if you are not using the static generated pages.

`; @@ -93,6 +104,7 @@ export class ScullyContentComponent implements OnInit, OnDestroy { console.error('problem during loading static scully content', e); }); } + /** insert the whole thing just before the `` element */ const parent = this.elm.parentElement || document.body; const begin = document.createComment('scullyContent-begin'); const end = document.createComment('scullyContent-end'); @@ -130,7 +142,7 @@ export class ScullyContentComponent implements OnInit, OnDestroy { return; } /** delete the content, as it is now out of date! */ - window['scullyContent'] = undefined; + window.scullyContent = undefined; /** check for the same route with different "data", and NOT a level higher (length) */ if (curSplit.every((part, i) => splitRoute[i] === part) && splitRoute.length > curSplit.length) { /** @@ -155,59 +167,9 @@ export class ScullyContentComponent implements OnInit, OnDestroy { } } - ngOnDestroy() { - // if (this.mutationSubscription) { - // this.mutationSubscription.unsubscribe(); - // } + getCSSId(elm: HTMLElement) { + return elm.getAttributeNames().find(a => a.startsWith('_ngcontent')) || 'none_found'; } -} -/** - * Returns an observable that fires a mutation when the domMutationObserves does that. - * if flattens the mutations to make handling easier, so you only get 1 mutationRecord at a time. - * @param elm the elm to obse with a mutationObserver - * @param config the config for the mutationobserver - */ -export function fromMutationObserver( - elm: HTMLElement, - config: MutationObserverInit -): Observable { - return new Observable(obs => { - const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation))); - observer.observe(elm, config); - return () => observer.disconnect(); - }); -} - -/** - * Returns an array of nodes coninting all the html comments in the element. - * When a searchText is given this is narrowed down to only comments that contian this text - * @param rootElem Element to search nto - * @param searchText optional string that needs to be in a HTML comment - */ -function findComments(rootElem: HTMLElement, searchText?: string) { - const comments = []; - // Fourth argument, which is actually obsolete according to the DOM4 standard, seems required in IE 11 - const iterator = document.createNodeIterator( - rootElem, - NodeFilter.SHOW_COMMENT, - { - acceptNode: node => { - // Logic to determine whether to accept, reject or skip node - // In this case, only accept nodes that have content - // that is containing our searchText, by rejecting any other nodes. - if (searchText && node.nodeValue && !node.nodeValue.includes(searchText)) { - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; - }, - } - // , false // IE-11 support requires this parameter. - ); - let curNode; - // tslint:disable-next-line: no-conditional-assignment - while ((curNode = iterator.nextNode())) { - comments.push(curNode); - } - return comments; + ngOnDestroy() {} } diff --git a/projects/scullyio/ng-lib/src/lib/utils/findComments.ts b/projects/scullyio/ng-lib/src/lib/utils/findComments.ts new file mode 100644 index 000000000..da9ab49b6 --- /dev/null +++ b/projects/scullyio/ng-lib/src/lib/utils/findComments.ts @@ -0,0 +1,32 @@ +/** + * Returns an array of nodes coninting all the html comments in the element. + * When a searchText is given this is narrowed down to only comments that contian this text + * @param rootElem Element to search nto + * @param searchText optional string that needs to be in a HTML comment + */ +export function findComments(rootElem: HTMLElement, searchText?: string) { + const comments = []; + // Fourth argument, which is actually obsolete according to the DOM4 standard, seems required in IE 11 + const iterator = document.createNodeIterator( + rootElem, + NodeFilter.SHOW_COMMENT, + { + acceptNode: node => { + // Logic to determine whether to accept, reject or skip node + // In this case, only accept nodes that have content + // that is containing our searchText, by rejecting any other nodes. + if (searchText && node.nodeValue && !node.nodeValue.includes(searchText)) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + } + // , false // IE-11 support requires this parameter. + ); + let curNode; + // tslint:disable-next-line: no-conditional-assignment + while ((curNode = iterator.nextNode())) { + comments.push(curNode); + } + return comments; +} diff --git a/projects/scullyio/ng-lib/src/lib/utils/fromMutationObserver.ts b/projects/scullyio/ng-lib/src/lib/utils/fromMutationObserver.ts new file mode 100644 index 000000000..551eee2d6 --- /dev/null +++ b/projects/scullyio/ng-lib/src/lib/utils/fromMutationObserver.ts @@ -0,0 +1,17 @@ +import {Observable} from 'rxjs'; +/** + * Returns an observable that fires a mutation when the domMutationObserves does that. + * if flattens the mutations to make handling easier, so you only get 1 mutationRecord at a time. + * @param elm the elm to obse with a mutationObserver + * @param config the config for the mutationobserver + */ +export function fromMutationObserver( + elm: HTMLElement, + config: MutationObserverInit +): Observable { + return new Observable(obs => { + const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation))); + observer.observe(elm, config); + return () => observer.disconnect(); + }); +} diff --git a/schematics/scully/src/files/blog-module/blog.component.ts b/schematics/scully/src/files/blog-module/blog.component.ts index c8c53d6a4..ad226167e 100644 --- a/schematics/scully/src/files/blog-module/blog.component.ts +++ b/schematics/scully/src/files/blog-module/blog.component.ts @@ -1,5 +1,5 @@ import {Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {ActivatedRoute, Router, ROUTES} from '@angular/router'; +import {ActivatedRoute, Router} from '@angular/router'; declare var ng: any; diff --git a/scully.sampleBlog.config.js b/scully.sampleBlog.config.js index 83494658c..52e4cc481 100644 --- a/scully.sampleBlog.config.js +++ b/scully.sampleBlog.config.js @@ -47,32 +47,15 @@ exports.config = { property: 'id', }, }, - '/todos/:todoId': { - // Type is mandatory - type: 'json', - /** - * Every parameter in the route must exist here - */ - todoId: { - url: 'https://jsonplaceholder.typicode.com/todos', - property: 'id', - /** - * Headers can be sent optionally - */ - headers: { - 'API-KEY': '0123456789', - }, - }, - }, '/blog/:slug': { type: 'contentFolder', - postRenderers: ['toc'], + // postRenderers: ['toc'], slug: { folder: './blog', }, }, '/**': { - type: 'void', + type: 'ignored', }, }, }; diff --git a/scully/renderPlugins/content-render-utils/getScript.ts b/scully/renderPlugins/content-render-utils/getScript.ts index e3123afe5..04de0ea5e 100644 --- a/scully/renderPlugins/content-render-utils/getScript.ts +++ b/scully/renderPlugins/content-render-utils/getScript.ts @@ -2,7 +2,7 @@ * @returns a string representing the script that parses the page and loads the scullyContent variable. * The string is kept on one line as the focus is to keep it as small as possible. */ -export function getScript(): string { +export function getScript(attr): string { // tslint:disable-next-line:no-unused-expression - return ``; + return ``; } diff --git a/scully/renderPlugins/contentRenderPlugin.ts b/scully/renderPlugins/contentRenderPlugin.ts index c72dfa786..ea1e2ffdd 100644 --- a/scully/renderPlugins/contentRenderPlugin.ts +++ b/scully/renderPlugins/contentRenderPlugin.ts @@ -5,6 +5,8 @@ import {getScript} from './content-render-utils/getScript'; import {handleFile} from './content-render-utils/handleFile'; import {insertContent} from './content-render-utils/insertContent'; import {readFileAndCheckPrePublishSlug} from './content-render-utils/readFileAndCheckPrePublishSlug'; +import {JSDOM} from 'jsdom'; +import {nodeModuleNameResolver} from 'typescript'; registerPlugin('render', 'contentFolder', contentRenderPlugin); @@ -18,8 +20,15 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) { const {meta, fileContent} = await readFileAndCheckPrePublishSlug(file, route); // TODO: create additional "routes" for every slug route.data = {...route.data, ...meta}; + const attr = getIdAttrName( + html + .split('')[0] + .trim() + ); const additionalHTML = await handleFile(extension, fileContent); - return insertContent(scullyBegin, scullyEnd, html, additionalHTML, getScript()); + const htmlWithNgAttr = addNgIdAttribute(additionalHTML, attr); + return insertContent(scullyBegin, scullyEnd, html, htmlWithNgAttr, getScript(attr)); } catch (e) { logWarn( `Error, probably missing "${yellow('')}" or "${yellow( @@ -28,3 +37,31 @@ export async function contentRenderPlugin(html: string, route: HandledRoute) { ); } } + +function addNgIdAttribute(html: string, id: string): string { + try { + const dom = new JSDOM(html, {runScripts: 'outside-only'}); + const {window} = dom; + const {document} = window; + const walk = document.createTreeWalker(document.body as any); + let cur = (walk.currentNode as any) as HTMLElement; + while (cur) { + if (cur.nodeType === 1) { + cur.setAttribute(id, ''); + } + cur = (walk.nextNode() as any) as HTMLElement; + } + return document.body.innerHTML; + } catch (e) { + console.error(e); + } + + return ''; +} + +function getIdAttrName(attrs: string): string { + return attrs + .split(' ') + .find((at: string) => at.trim().startsWith('_ngcontent')) + .split('=')[0]; +}