Skip to content
Open
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
37 changes: 21 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
{
"name": "pixelpaper",
"version": "0.0.1",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"dev:web": "npm run dev --workspace=@pixelpaper/web",
"build:web": "npm run build --workspace=@pixelpaper/web",
"preview:web": "npm run preview --workspace=@pixelpaper/web",
"dev:desktop": "cd packages/desktop && node server.js"
},
"dependencies": {
"figlet": "^1.10.0",
"ws": "^8.19.0"
}
"name": "pixelpaper",
"version": "0.0.1",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"dev:web": "npm run dev --workspace=@pixelpaper/web",
"build:web": "npm run build --workspace=@pixelpaper/web",
"preview:web": "npm run preview --workspace=@pixelpaper/web",
"dev:desktop": "cd packages/desktop && node server.js"
},
"dependencies": {
"figlet": "^1.10.0",
"native-file-system-adapter": "^3.0.1",
"unist-util-visit": "^5.1.0",
"ws": "^8.19.0"
},
"devDependencies": {
"@types/node": "^25.3.0"
}
}
69 changes: 69 additions & 0 deletions packages/core/src/ProjectData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
FileSystemDirectoryHandle,
FileSystemFileHandle,
} from "native-file-system-adapter";

export type ProjectFileType = {
name: string;
path: string;
content: string | Blob;
fileHandle?: FileSystemFileHandle | false;
extension?: TextExtType | ImageExtType;
};

type TextExtType = "md" | "css"; // the keys you expect
type ImageExtType = "jpg" | "jpeg" | "png" | "gif" | "webp" | "svg";

export const allowedTextExtensions: TextExtType[] = ["md", "css"];
export const allowedImageExtensions: ImageExtType[] = [
"jpg",
"jpeg",
"png",
"gif",
"webp",
"svg",
];

export class ProjectData {
handle: FileSystemDirectoryHandle | false = false;
md: ProjectFileType[] = [];
css: ProjectFileType[] = [];
images: Record<string, ProjectFileType> = {};
orderDeduper: ReturnType<typeof setTimeout> | false = false;
constructor() {}
reset(): void {
this.handle = false;
this.md = [];
this.css = [];
this.images = {};
}
sort(): void {
this.css.sort((a, b) => a.name.localeCompare(b.name));
this.md.sort((a, b) => a.name.localeCompare(b.name));
}
store(data: ProjectFileType | ProjectFileType[]): void {
const projectFiles = Array.isArray(data) ? data : [data];
for (const file of projectFiles) {
const newFile: ProjectFileType = {
name: file.name,
path: file.path,
content: file.content ? file.content : "",
fileHandle: file.fileHandle,
extension: file.extension
? file.extension
: (file.path
.toLowerCase()
.substring(file.path.lastIndexOf(".") + 1)
.toLowerCase() as TextExtType | ImageExtType),
};
if (allowedTextExtensions.includes(newFile.extension as TextExtType)) {
this[newFile.extension as TextExtType].push(newFile);
this.sort();
} else if (
allowedImageExtensions.includes(newFile.extension as ImageExtType)
) {
this.images[newFile.path] = newFile;
}
}
}
}
52 changes: 25 additions & 27 deletions packages/core/src/config.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,40 @@
import { markdown } from "@codemirror/lang-markdown";
import { css } from "@codemirror/lang-css";
import { loadFile } from "@core/controllers/loadFile.js";
import { ProjectData } from "./ProjectData";

// CONFIG Defines the default project
export const TEMPLATE = [
{ type: "css", name: "Page", data: ["1_page.css"] },
{ type: "css", name: "Setup", data: ["2_setup.css"] },
{ type: "css", name: "interface", data: ["3_interface.css"] },
{ type: "css", name: "Variables", data: ["4_variables.css"] },
{ type: "css", name: "Typography", data: ["5_typography.css"] },
{ type: "css", name: "Structure", data: ["6_structure.css"] },
{ type: "md", name: "Intro", data: ["1_intro.md"] },
{ type: "md", name: "Body", data: ["2_body.md"] },
{ type: "css", name: "Page", data: ["1_page.css"] },
{ type: "css", name: "Setup", data: ["2_setup.css"] },
{ type: "css", name: "interface", data: ["3_interface.css"] },
{ type: "css", name: "Variables", data: ["4_variables.css"] },
{ type: "css", name: "Typography", data: ["5_typography.css"] },
{ type: "css", name: "Structure", data: ["6_structure.css"] },
{ type: "md", name: "Intro", data: ["1_intro.md"] },
{ type: "md", name: "Body", data: ["2_body.md"] },
{ type: "image", name: "Le petit poucet", data: ["le_petit_poucet.jpg"] },
];

export const BASE_PATH = "/data/";
export const BASE_PATH = "/api/templates/";

// Create DATA from config
export const templateData = async () => {
const PROMISES = TEMPLATE.map(async ({ type, name, data }) => {
const content = await loadFile(`${BASE_PATH}${data[0]}`);
export const templateData = async (template) => {
const PROMISES = TEMPLATE.map(async ({ type, name, data }) => {
const content = await loadFile(`${BASE_PATH}${template}/${data[0]}`);

return { type: type, name: name, filename: data[0], content:content };
});
return { type: type, name: name, filename: data[0], content: content };
});

// DATA is an array of {type: 'type, for example css or md', data: 'string content'}
const projectDataFromTemplate = await Promise.all(PROMISES);
return projectDataFromTemplate;
}

export const projectData = {
handle: '', // Where are files stored
md: [],
css: [],
images: {}, // Key: "media/filename.jpg", Value: "blob:..."
// DATA is an array of {type: 'type, for example css or md', data: 'string content'}
const projectDataFromTemplate = await Promise.all(PROMISES);
return projectDataFromTemplate;
};

// export an instance of ProjectData
export const projectData = new ProjectData();

export const languageMap = {
css: css,
md: markdown,
};
css: css,
md: markdown,
};
26 changes: 14 additions & 12 deletions packages/core/src/controllers/loadFile.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// Simple file laoder
// Simple file loader
export const loadFile = async (filePath) => {
try {
// Request the file from the local server
const response = await fetch(filePath);

// Convert the response to text
const content = await response.text();

return content;
} catch (err) {
console.error("Error reading file:", err);
try {
// Request the file from the server
const response = await fetch(filePath);
if (response.headers.get("content-type").includes("image")) {
// Convert image file response to blob
return await response.blob();
} else {
// Convert the response to text
return await response.text();
}
};
} catch (err) {
console.error("Error reading file:", err);
}
};
139 changes: 87 additions & 52 deletions packages/core/src/markdown/iframeHtml.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,79 @@
import {unified} from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
// import rehypeFormat from 'https://esm.sh/rehype-format';
import rehypeStringify from 'rehype-stringify'
import rehypeStringify from "rehype-stringify";
import remarkSection from "./remark-section.js";
import remarkGfm from 'remark-gfm';
import { remarkExtendImage } from './remark-figure.js';
import remarkDirectiveRehype from './rehype-custom-directives.js';
import remarkDirective from 'remark-directive'; // ⚠️ Required to parse ::: syntax

export const renderMarkdown = async (md) => {
console.time('Markdown Fetch & Process');


// Convert Markdown to HTML using unified
const htmlText = await unified()
.use(remarkParse) // Markdown to AST
.use(remarkGfm) // Git flavored MD
.use(remarkDirective)
.use(remarkDirectiveRehype)
.use(remarkSection) // Add sections to H tags
.use(remarkExtendImage) // Images to figures
.use(remarkRehype, { allowDangerousHtml: true }) // AST to HTML
// .use(rehypeFormat) // Pretty print HTML
.use(rehypeStringify, { allowDangerousHtml: true }) // Convert AST to string
.process(md); // Wait for this step to complete

console.timeEnd('Markdown Fetch & Process'); // End time for markdown fetch and processing

return htmlText;
}
import remarkGfm from "remark-gfm";
import { remarkExtendImage } from "./remark-figure.js";
import remarkDirectiveRehype from "./rehype-custom-directives.js";
import remarkDirective from "remark-directive"; // ⚠️ Required to parse ::: syntax
import { visit } from "unist-util-visit";

export const renderMarkdown = async (md, images = {}) => {
console.time("Markdown Fetch & Process");

// Convert Markdown to HTML using unified
const htmlText = await unified()
.use(remarkParse) // Markdown to AST
.use(remarkGfm) // Git flavored MD
.use(remarkDirective)
.use(remarkDirectiveRehype)
.use(remarkSection) // Add sections to H tags
.use(cachedImageToUrl(images))
.use(remarkExtendImage) // Images to figures
.use(remarkRehype, { allowDangerousHtml: true }) // AST to HTML
// .use(rehypeFormat) // Pretty print HTML
.use(rehypeStringify, { allowDangerousHtml: true }) // Convert AST to string
.process(md); // Wait for this step to complete

console.timeEnd("Markdown Fetch & Process"); // End time for markdown fetch and processing

return htmlText;
};

// Encode image data into URL to make preview from memory work.
function cachedImageToUrl(images) {
return () => (tree) => {
visit(tree, "image", (node) => {
const key = String(node.url).trim().replace(/^\/+/, "");
const cachedImage = images[key];
if (!cachedImage) return;
node.url = URL.createObjectURL(cachedImage.content);
});
};
}

////////////////////////////////////////
// takes array of css and single md file
// takes array of css and single md file
// returns a string containing the iframe
export const iframeHtml = async (css, md, pagedPolyfill, htlmContent = '', jsContent = '') => {
// This needs error checking
const allImportedStyles = Array.isArray(css) ? css : [css];

const allMdContent = Array.isArray(md) ? md.join('\n') : md;

const allStyles = allImportedStyles.map(style => ` <style>${style}</style>`).join('');

let renderedHtmlfromMarkdown = '';
if (allMdContent) {
const processed = await renderMarkdown(allMdContent);
// CRITICAL FIX: Check if it's an object or string
renderedHtmlfromMarkdown = (typeof processed === 'string')
? processed
: processed.value; // Access the VFile content
}
const renderedHtml = renderedHtmlfromMarkdown + htlmContent;
export const iframeHtml = async (
css,
md,
images,
pagedPolyfill,
htlmContent = "",
jsContent = "",
) => {
// This needs error checking
const allImportedStyles = Array.isArray(css) ? css : [css];

const allMdContent = Array.isArray(md) ? md.join("\n") : md;

const allStyles = allImportedStyles
.map((style) => ` <style>${style}</style>`)
.join("");

let renderedHtmlfromMarkdown = "";
if (allMdContent) {
const processed = await renderMarkdown(allMdContent, images);
// CRITICAL FIX: Check if it's an object or string
renderedHtmlfromMarkdown =
typeof processed === "string" ? processed : processed.value; // Access the VFile content
}
const renderedHtml = renderedHtmlfromMarkdown + htlmContent;

const fullHtmlContent = `
const fullHtmlContent = `
<!DOCTYPE html>
<html>
<head>
Expand All @@ -61,6 +82,18 @@ export const iframeHtml = async (css, md, pagedPolyfill, htlmContent = '', jsCon

<script>${pagedPolyfill}</script>
<script>

class iframeBeforeRender extends Paged.Handler {
beforeRender(root) {
const blob = new Blob([myImageBuffer], { type: "image/png" });
const url = URL.createObjectURL(blob);

const img = document.createElement("img");
img.src = url;
root.querySelector("#placeholder").replaceWith(img);
}
}

class iframeRendered extends Paged.Handler {

afterRendered(pages) {
Expand All @@ -70,6 +103,8 @@ export const iframeHtml = async (css, md, pagedPolyfill, htlmContent = '', jsCon

Paged.registerHandlers(iframeRendered);

Paged.registerHandlers();

</script>
${jsContent.trim()}
${allStyles.trim()}
Expand All @@ -80,7 +115,7 @@ export const iframeHtml = async (css, md, pagedPolyfill, htlmContent = '', jsCon
</html>
`;

/*
/*
const safeDataUrl = "data:text/html;charset=utf-8," + encodeURIComponent(fullHtmlContent);

const econdedIframe = `<iframe width="100%" height="100%" src="${safeDataUrl}"></iframe>`;
Expand All @@ -95,5 +130,5 @@ const blobUrl = URL.createObjectURL(blob);
const encodedIframe = `<iframe width="100%" height="100%" src="${blobUrl}" style="border:none;"></iframe>`;
*/

return fullHtmlContent;
}
return fullHtmlContent;
};
Loading