Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"cSpell.words": ["Scully", "Scullyio"],
"cSpell.words": ["Scully", "Scullyio", "ngcontent"],
"peacock.affectActivityBar": true,
"peacock.affectTitleBar": true,
"peacock.affectStatusBar": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand Down
2 changes: 1 addition & 1 deletion projects/sampleBlog/src/app/blog/blog.component.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
::slotted(h1) {
h1 {
color: rgb(51, 6, 37);
background-color: rgb(248, 211, 236);
padding: 5px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<!--scullyContent-begin-->';
const scullyEnd = '<!--scullyContent-end-->';
Expand All @@ -36,22 +44,15 @@ const scullyEnd = '<!--scullyContent-end-->';
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();
Expand All @@ -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;
/**
Expand All @@ -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 = `<h2 id="___scully-parsing-error___">Sorry, could not parse static page content</h2>
<p>This might happen if you are not using the static generated pages.</p>`;
Expand All @@ -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 `<scully-content>` element */
const parent = this.elm.parentElement || document.body;
const begin = document.createComment('scullyContent-begin');
const end = document.createComment('scullyContent-end');
Expand Down Expand Up @@ -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) {
/**
Expand All @@ -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<MutationRecord> {
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() {}
}
32 changes: 32 additions & 0 deletions projects/scullyio/ng-lib/src/lib/utils/findComments.ts
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions projects/scullyio/ng-lib/src/lib/utils/fromMutationObserver.ts
Original file line number Diff line number Diff line change
@@ -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<MutationRecord> {
return new Observable(obs => {
const observer = new MutationObserver(mutations => mutations.forEach(mutation => obs.next(mutation)));
observer.observe(elm, config);
return () => observer.disconnect();
});
}
2 changes: 1 addition & 1 deletion schematics/scully/src/files/blog-module/blog.component.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
21 changes: 2 additions & 19 deletions scully.sampleBlog.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
};
4 changes: 2 additions & 2 deletions scully/renderPlugins/content-render-utils/getScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<script>try {window['scullyContent'] = document.body.innerHTML.split('<!--scullyContent-begin-->')[1].split('<!--scullyContent-end-->')[0];} catch(e) {console.error('scully could not parse content',e);}</script>`;
return `<script>try {window['scullyContent'] = {cssId:"${attr}",html:document.body.innerHTML.split('<!--scullyContent-begin-->')[1].split('<!--scullyContent-end-->')[0]};} catch(e) {console.error('scully could not parse content');}</script>`;
}
39 changes: 38 additions & 1 deletion scully/renderPlugins/contentRenderPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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('<scully-content')[1]
.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('<scully-content>')}" or "${yellow(
Expand All @@ -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];
}