presi-slides 1.0.0
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.
- package/LICENSE +28 -0
- package/dist/presi-slides.cjs +111 -0
- package/dist/presi-slides.js +8538 -0
- package/dist/prism-coy-without-shadows.css +143 -0
- package/dist/prism-theme.css +144 -0
- package/dist/slides.css +1456 -0
- package/eleventy.config.js +48 -0
- package/index.html +42 -0
- package/layouts/presentation.njk +23 -0
- package/package.json +38 -0
- package/public/prism-coy-without-shadows.css +143 -0
- package/public/prism-theme.css +144 -0
- package/public/slides.css +1456 -0
- package/slide-decks/Sample Presi.textbundle/assets/image 6.png +0 -0
- package/slide-decks/Sample Presi.textbundle/info.json +10 -0
- package/slide-decks/Sample Presi.textbundle/text.md +45 -0
- package/slide-decks/slide-decks.11tydata.json +3 -0
- package/src/index.js +10 -0
- package/src/slide-footer.js +109 -0
- package/src/slide-presi.js +120 -0
- package/src/slide-view.js +142 -0
- package/src/slides.js +18 -0
- package/transforms/slides.js +75 -0
- package/vite.config.js +9 -0
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sample Presi
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Sample Presi
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Slide One
|
|
10
|
+
|
|
11
|
+
- This is a simple slide
|
|
12
|
+
- It just has bullets
|
|
13
|
+
- It's boring, but it works.
|
|
14
|
+
- Time for more fun
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Slide Two
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
- This slide has a picture
|
|
25
|
+
- There are also bullets
|
|
26
|
+
- They should be side by side
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# Slide 3
|
|
31
|
+
|
|
32
|
+
### Some Bullets
|
|
33
|
+
|
|
34
|
+
- Thing 1
|
|
35
|
+
- Thing 2
|
|
36
|
+
- Thing 3
|
|
37
|
+
|
|
38
|
+
### A Table
|
|
39
|
+
|
|
40
|
+
| Row Number | Thing |
|
|
41
|
+
|------------|----------|
|
|
42
|
+
| 1 | Parsley |
|
|
43
|
+
| 2 | Sage |
|
|
44
|
+
| 3 | Rosemary |
|
|
45
|
+
| 4 | Thyme |
|
package/src/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { css, html, shadow } from "@unbndl/html";
|
|
2
|
+
|
|
3
|
+
export class FooterElement extends HTMLElement {
|
|
4
|
+
static template = html`
|
|
5
|
+
<template>
|
|
6
|
+
<footer>
|
|
7
|
+
<fieldset name="overlay">
|
|
8
|
+
<label>
|
|
9
|
+
<input type="radio" name="overlay" value="pointer" checked>
|
|
10
|
+
<span>Select</span>
|
|
11
|
+
</label>
|
|
12
|
+
<label>
|
|
13
|
+
<input type="radio" name="overlay" value="brush">
|
|
14
|
+
<span>Brush</span>
|
|
15
|
+
</label>
|
|
16
|
+
<label>
|
|
17
|
+
<input type="radio" name="overlay" value="eraser">
|
|
18
|
+
<span>Eraser</span>
|
|
19
|
+
</label>
|
|
20
|
+
<label>
|
|
21
|
+
<input type="radio" name="overlay" value="hidden">
|
|
22
|
+
<span>Hide</span>
|
|
23
|
+
</label>
|
|
24
|
+
</fieldset>
|
|
25
|
+
<span>
|
|
26
|
+
<input name="slide" type="number" min="1" value="1">
|
|
27
|
+
/
|
|
28
|
+
<slot name="count">N</span>
|
|
29
|
+
</span>
|
|
30
|
+
</footer>
|
|
31
|
+
</template>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
static styles = css`
|
|
35
|
+
footer {
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: space-between;
|
|
38
|
+
color: var(--color-text-inverted);
|
|
39
|
+
}
|
|
40
|
+
span {
|
|
41
|
+
background: rgb(0 0 0 / 0.8);
|
|
42
|
+
border-radius: var(--size-spacing-small);
|
|
43
|
+
padding: var(--size-spacing-small);
|
|
44
|
+
}
|
|
45
|
+
input,
|
|
46
|
+
select {
|
|
47
|
+
font: inherit;
|
|
48
|
+
color: inherit;
|
|
49
|
+
line-height: inherit;
|
|
50
|
+
border: none;
|
|
51
|
+
border-radius: 1px;
|
|
52
|
+
background: transparent;
|
|
53
|
+
}
|
|
54
|
+
input[type="number"] {
|
|
55
|
+
width: 3em;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
slideControl = null;
|
|
60
|
+
overlayControl = null;
|
|
61
|
+
|
|
62
|
+
constructor() {
|
|
63
|
+
super();
|
|
64
|
+
|
|
65
|
+
shadow(this)
|
|
66
|
+
.template(FooterElement.template)
|
|
67
|
+
.styles(FooterElement.styles)
|
|
68
|
+
.delegate('input[name="slide"]', {
|
|
69
|
+
input: (ev) =>
|
|
70
|
+
this.relay("slide:input", { slide: ev.target.value })
|
|
71
|
+
})
|
|
72
|
+
.delegate('input[name="overlay"]', {
|
|
73
|
+
change: (ev) =>
|
|
74
|
+
this.relay("slide:overlay", { overlay: ev.target.value })
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.slideControl = this.shadowRoot.querySelector(
|
|
78
|
+
'input[name="slide"]'
|
|
79
|
+
);
|
|
80
|
+
this.overlayControl = this.shadowRoot.querySelector(
|
|
81
|
+
'fieldset[name="overlay"]'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static observedAttributes = ["slide", "overlay", "color"];
|
|
87
|
+
|
|
88
|
+
attributeChangedCallback(name, _, newValue) {
|
|
89
|
+
switch (name) {
|
|
90
|
+
case "slide":
|
|
91
|
+
this.slideControl.value = newValue;
|
|
92
|
+
break;
|
|
93
|
+
case "overlay": {
|
|
94
|
+
const input = this.overlayControl.querySelector(`input[value="${newValue}"]`);
|
|
95
|
+
if (input) input.checked = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
relay(type, detail) {
|
|
102
|
+
const event = new CustomEvent(type, {
|
|
103
|
+
bubbles: true,
|
|
104
|
+
composed: true,
|
|
105
|
+
detail
|
|
106
|
+
});
|
|
107
|
+
this.dispatchEvent(event);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { html } from "@unbndl/html";
|
|
2
|
+
|
|
3
|
+
const MIN_INTERSECTION = 0.4;
|
|
4
|
+
const MAX_INTERSECTION = 0.8;
|
|
5
|
+
|
|
6
|
+
export class PresiElement extends HTMLElement {
|
|
7
|
+
VIEW_ELEMENT = "slide-view";
|
|
8
|
+
FOOTER_ELEMENT = "slide-footer";
|
|
9
|
+
|
|
10
|
+
controls = null;
|
|
11
|
+
active = null;
|
|
12
|
+
|
|
13
|
+
observer = new IntersectionObserver(
|
|
14
|
+
(entries) => {
|
|
15
|
+
let maxIntersection = 0;
|
|
16
|
+
let maxElement = null;
|
|
17
|
+
entries.forEach((entry) => {
|
|
18
|
+
if (entry.isIntersecting) {
|
|
19
|
+
const element = entry.target;
|
|
20
|
+
//console.log("Intersection:", element, entry.intersectionRatio);
|
|
21
|
+
if (entry.intersectionRatio < MIN_INTERSECTION) {
|
|
22
|
+
element.removeAttribute("active");
|
|
23
|
+
} else {
|
|
24
|
+
if (entry.intersectionRatio > maxIntersection) {
|
|
25
|
+
maxIntersection = entry.intersectionRatio;
|
|
26
|
+
maxElement = element;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (maxElement && maxElement !== this.active) {
|
|
33
|
+
this.setActiveSlide(maxElement);
|
|
34
|
+
// this.gotoSlide(maxElement);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
threshold: [0, MIN_INTERSECTION, MAX_INTERSECTION, 1],
|
|
39
|
+
delay: 300
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
this.addEventListener("slide:input", (ev) => {
|
|
46
|
+
const { slide } = ev.detail;
|
|
47
|
+
const element = this.slide(slide);
|
|
48
|
+
// console.log("Change Slide:", slide, element);
|
|
49
|
+
this.setActiveSlide(element);
|
|
50
|
+
this.jumpToSlide(element);
|
|
51
|
+
});
|
|
52
|
+
this.addEventListener("slide:overlay", (ev) => {
|
|
53
|
+
this.manageOverlay(ev.detail);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
slide(n) {
|
|
58
|
+
const slides = Array.from(
|
|
59
|
+
this.querySelectorAll(this.VIEW_ELEMENT)
|
|
60
|
+
);
|
|
61
|
+
return slides[n - 1];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
connectedCallback() {
|
|
65
|
+
const slides = Array.from(
|
|
66
|
+
this.querySelectorAll(this.VIEW_ELEMENT)
|
|
67
|
+
);
|
|
68
|
+
this.controls = this.querySelector(this.FOOTER_ELEMENT);
|
|
69
|
+
slides.forEach((target) => this.observer.observe(target));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
gotoSlide(element) {
|
|
73
|
+
console.log("Going to slide", element);
|
|
74
|
+
if (element) {
|
|
75
|
+
element.scrollIntoView({
|
|
76
|
+
behavior: "smooth",
|
|
77
|
+
block: "center",
|
|
78
|
+
inline: "nearest"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
jumpToSlide(element) {
|
|
84
|
+
if (element) {
|
|
85
|
+
element.scrollIntoView({
|
|
86
|
+
block: "center",
|
|
87
|
+
inline: "nearest"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setActiveSlide(element) {
|
|
93
|
+
//console.log("Activating slide", element);
|
|
94
|
+
this.active = element;
|
|
95
|
+
if (this.active) {
|
|
96
|
+
this.active.setAttribute("active", "active");
|
|
97
|
+
const slides = Array.from(
|
|
98
|
+
this.querySelectorAll(this.VIEW_ELEMENT)
|
|
99
|
+
);
|
|
100
|
+
const index = slides.findIndex(
|
|
101
|
+
(slide) => slide === element
|
|
102
|
+
);
|
|
103
|
+
if (this.controls) {
|
|
104
|
+
this.controls.setAttribute("slide", index + 1);
|
|
105
|
+
this.controls.setAttribute(
|
|
106
|
+
"overlay",
|
|
107
|
+
element.getAttribute("overlay") || "pointer"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
manageOverlay(options) {
|
|
114
|
+
const { overlay } = options || {};
|
|
115
|
+
console.log("Overlay options", options);
|
|
116
|
+
if (this.active && overlay) {
|
|
117
|
+
this.active.setAttribute("overlay", overlay);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { html, css, shadow } from "@unbndl/html";
|
|
2
|
+
import Konva from 'konva';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class SlideElement extends HTMLElement {
|
|
6
|
+
static template = html`
|
|
7
|
+
<template>
|
|
8
|
+
<slot></slot>
|
|
9
|
+
<div id="overlay"></div>
|
|
10
|
+
</template>
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
static styles = css`
|
|
14
|
+
:host {
|
|
15
|
+
--slide-pointer-events: none;
|
|
16
|
+
--slide-opacity: 1;
|
|
17
|
+
--slide-overlay-visibility: hidden;
|
|
18
|
+
/*position: relative;*/
|
|
19
|
+
}
|
|
20
|
+
@media screen {
|
|
21
|
+
:host {
|
|
22
|
+
--slide-opacity: 1; // make it 0.2 for fading effect on scroll;
|
|
23
|
+
--slide-overlay-visibility: visible;
|
|
24
|
+
}
|
|
25
|
+
:host([active]) {
|
|
26
|
+
--slide-opacity: 1;
|
|
27
|
+
}
|
|
28
|
+
:host([overlay]) {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
:host([overlay="hidden"]) {
|
|
32
|
+
--slide-overlay-visibility: hidden;
|
|
33
|
+
}
|
|
34
|
+
:host([overlay="brush"]) {
|
|
35
|
+
cursor: url("/includes/pencil.png") 0 100, crosshair;
|
|
36
|
+
--slide-pointer-events: auto;
|
|
37
|
+
}
|
|
38
|
+
:host([overlay="eraser"]) {
|
|
39
|
+
cursor: not-allowed;
|
|
40
|
+
--slide-pointer-events: auto;
|
|
41
|
+
}
|
|
42
|
+
#overlay {
|
|
43
|
+
position: absolute;
|
|
44
|
+
inset: 0;
|
|
45
|
+
pointer-events: var(--slide-pointer-events);
|
|
46
|
+
visibility: var(--slide-overlay-visibility);
|
|
47
|
+
}
|
|
48
|
+
::slotted(*) {
|
|
49
|
+
opacity: var(--slide-opacity);
|
|
50
|
+
transition: opacity 1s;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
// initialize the Konva overlay
|
|
56
|
+
isPaint = false;
|
|
57
|
+
overlayMode = "pointer";
|
|
58
|
+
overlayColor = "red";
|
|
59
|
+
stage = null;
|
|
60
|
+
|
|
61
|
+
constructor() {
|
|
62
|
+
super();
|
|
63
|
+
|
|
64
|
+
shadow(this)
|
|
65
|
+
.template(SlideElement.template)
|
|
66
|
+
.styles(SlideElement.styles);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static observedAttributes = ["active", "overlay"];
|
|
70
|
+
|
|
71
|
+
attributeChangedCallback(name, _, newValue) {
|
|
72
|
+
switch (name) {
|
|
73
|
+
case "overlay":
|
|
74
|
+
this.overlayMode = newValue;
|
|
75
|
+
this.createOverlay();
|
|
76
|
+
break;
|
|
77
|
+
case "color":
|
|
78
|
+
this.overlayColor = newValue;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disconnectedCallback() {
|
|
84
|
+
if (this.stage) delete this.stage;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createOverlay() {
|
|
88
|
+
if (this.stage) return;
|
|
89
|
+
|
|
90
|
+
const container = this.shadowRoot.getElementById("overlay");
|
|
91
|
+
|
|
92
|
+
console.log("Creating overlay if needed", this.stage);
|
|
93
|
+
|
|
94
|
+
this.stage = new Konva.Stage({
|
|
95
|
+
container,
|
|
96
|
+
width: container?.clientWidth,
|
|
97
|
+
height: container?.clientHeight
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.layer = new Konva.Layer();
|
|
101
|
+
this.stage.add(this.layer);
|
|
102
|
+
|
|
103
|
+
this.stage.on("mousedown touchstart", (e) => {
|
|
104
|
+
this.isPaint = true;
|
|
105
|
+
const pos = this.stage.getPointerPosition();
|
|
106
|
+
console.log("touchstart", pos);
|
|
107
|
+
this.lastLine = new Konva.Line({
|
|
108
|
+
stroke: this.overlayColor,
|
|
109
|
+
strokeWidth: this.overlayMode === "eraser" ? 10 : 4,
|
|
110
|
+
globalCompositeOperation:
|
|
111
|
+
this.overlayMode === "brush"
|
|
112
|
+
? "source-over"
|
|
113
|
+
: "destination-out",
|
|
114
|
+
// round cap for smoother lines
|
|
115
|
+
lineCap: "round",
|
|
116
|
+
lineJoin: "round",
|
|
117
|
+
// add point twice, so we have some drawings even on a simple click
|
|
118
|
+
points: [pos.x, pos.y, pos.x, pos.y]
|
|
119
|
+
});
|
|
120
|
+
this.layer.add(this.lastLine);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.stage.on("mouseup touchend", () => {
|
|
124
|
+
this.isPaint = false;
|
|
125
|
+
console.log("touchend");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// and core function - drawing
|
|
129
|
+
this.stage.on("mousemove touchmove", (e) => {
|
|
130
|
+
if (!this.isPaint) return;
|
|
131
|
+
|
|
132
|
+
// prevent scrolling on touch devices
|
|
133
|
+
e.evt.preventDefault();
|
|
134
|
+
|
|
135
|
+
const pos = this.stage.getPointerPosition();
|
|
136
|
+
const newPoints = this.lastLine
|
|
137
|
+
.points()
|
|
138
|
+
.concat([pos.x, pos.y]);
|
|
139
|
+
this.lastLine.points(newPoints);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/slides.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./slide-footer.js";
|
|
2
|
+
export * from "./slide-view.js";
|
|
3
|
+
export * from "./slide-presi.js";
|
|
4
|
+
|
|
5
|
+
export function scaleSlides() {
|
|
6
|
+
window.addEventListener("resize", computeSlideScaling);
|
|
7
|
+
computeSlideScaling();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function computeSlideScaling() {
|
|
11
|
+
const body = document.body;
|
|
12
|
+
const vw = document.documentElement.clientWidth;
|
|
13
|
+
const vh = document.documentElement.clientHeight;
|
|
14
|
+
body.classList.add("presentation-scaled");
|
|
15
|
+
body.style.setProperty("--viewport-width", vw.toString());
|
|
16
|
+
body.style.setProperty("--viewport-height", vh.toString());
|
|
17
|
+
console.log("Rescaled", vw, vh);
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { JSDOM } from "jsdom";
|
|
3
|
+
|
|
4
|
+
const PRESI_COMPONENT = "slide-presi";
|
|
5
|
+
const FOOTER_COMPONENT = "slide-footer";
|
|
6
|
+
const SLIDE_COMPONENT = "slide-view";
|
|
7
|
+
|
|
8
|
+
// JSDOM cannot parse modern CSS stylesheets.
|
|
9
|
+
// Workaround from: https://github.com/jsdom/jsdom/issues/3236#issuecomment-1699777509
|
|
10
|
+
|
|
11
|
+
const originalConsoleError = console.error;
|
|
12
|
+
console.error = (message, ...optionalParams) => {
|
|
13
|
+
if (message.includes("Could not parse CSS stylesheet")) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
originalConsoleError(message, ...optionalParams);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function (content) {
|
|
20
|
+
const { inputPath, outputPath } = this.page;
|
|
21
|
+
const inputDir = path.dirname(inputPath);
|
|
22
|
+
const inputFile = path.basename(inputPath);
|
|
23
|
+
const outputDir = path.dirname(outputPath);
|
|
24
|
+
const outputBase = path.dirname(outputDir);
|
|
25
|
+
const outputFile = path.basename(outputPath);
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
inputPath &&
|
|
29
|
+
inputDir.endsWith(".textbundle") &&
|
|
30
|
+
inputFile === "text.md"
|
|
31
|
+
) {
|
|
32
|
+
console.log("Processing slides in...", inputDir);
|
|
33
|
+
|
|
34
|
+
const DOM = new JSDOM(content);
|
|
35
|
+
const { document } = DOM.window;
|
|
36
|
+
const body = document.querySelector("body");
|
|
37
|
+
const children = Array.from(body.children);
|
|
38
|
+
const parts = children.reduce(
|
|
39
|
+
(acc, val) => {
|
|
40
|
+
let current = acc.length ? acc[acc.length - 1] : [];
|
|
41
|
+
let previous = acc;
|
|
42
|
+
if (val.tagName === "HR") {
|
|
43
|
+
current = [];
|
|
44
|
+
} else {
|
|
45
|
+
current = current.concat([val]);
|
|
46
|
+
previous = previous.slice(0, -1);
|
|
47
|
+
}
|
|
48
|
+
return previous.concat([current]);
|
|
49
|
+
},
|
|
50
|
+
[[]]
|
|
51
|
+
);
|
|
52
|
+
const count = parts.length;
|
|
53
|
+
const slides = parts.map((children, i) => {
|
|
54
|
+
const section = document.createElement(SLIDE_COMPONENT);
|
|
55
|
+
section.replaceChildren(...children);
|
|
56
|
+
return section;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const footer = document.createElement(FOOTER_COMPONENT);
|
|
60
|
+
const countSpan = document.createElement('span');
|
|
61
|
+
countSpan.textContent = count.toString();
|
|
62
|
+
countSpan.setAttribute("slot", "count");
|
|
63
|
+
footer.replaceChildren(countSpan);
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
const presi = document.createElement(PRESI_COMPONENT);
|
|
67
|
+
presi.replaceChildren(...slides, footer);
|
|
68
|
+
|
|
69
|
+
body.replaceChildren(presi);
|
|
70
|
+
|
|
71
|
+
return `<!doctype html>${document.documentElement.outerHTML}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return content;
|
|
75
|
+
}
|