Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas committed Aug 7, 2024
1 parent ff3896f commit 91ee4bd
Show file tree
Hide file tree
Showing 13 changed files with 460 additions and 121 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.idea
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ dist
index.html
main.js
vite.config.js
.idea
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# HTML To Table

> DEMO: https://w99910.github.io/html-to-table/
### It's zero dependency.

I have been trying to send emails using html layout but there are lots of html elements and styles that email clients
Expand Down Expand Up @@ -40,3 +42,7 @@ html2table.convert(document.querySelector('your-element-to-convert'));
```

## MIT LICENSE

## CHANGELOG

- **0.1.0** - Rewrite the logic by using bounding client rect to determine the layout
1 change: 1 addition & 0 deletions dist/html-to-table.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"use strict";const m={supportedCssProperties:["backgroundColor","background","backgroundImage","backgroundPosition","border(.)+","margin(.)+","padding(.)+","width","height","maxWidth","maxHeight","color","fontSize","font","fontWeight","textAlign","textDecoration","-webkitTextDecorationColor","textIndent","textTransform","letterSpacing","listStyleType","listStylePosition"],supportedHTMLTags:["p","span","h1","h2","h3","h4","h5","h6","br","strong","em","blockquote","ul","li","ol","pre","a","image","img","svg","table","td","tr","tbody","code"]};function b(u,i){let t=u.split(" ")[0];const n=parseFloat(t);if(isNaN(n))return!1;if(t.endsWith("px"))return n;if(t.endsWith("%"))return!1;if(t.endsWith("em")||t.endsWith("rem")){const s=parseFloat(getComputedStyle(i).fontSize);return n*s}else if(t.endsWith("vw")){const s=window.innerWidth;return n/100*s}else if(t.endsWith("vh")){const s=window.innerHeight;return n/100*s}else if(t.endsWith("vmin")){const s=Math.min(window.innerWidth,window.innerHeight);return n/100*s}else if(t.endsWith("vmax")){const s=Math.max(window.innerWidth,window.innerHeight);return n/100*s}else return n}class w{constructor(){this.supportedCSSProperties=m.supportedCssProperties}parse(i,o=!1){if(!i)return{};let t=window.getComputedStyle(i),n={};switch(n.tableAlign="left",t.textAlign!=="start"&&(n.tableAlign=t.textAlign),n.tableVAlign="top",t.justifyContent){case"start":n.tableVAlign="top";break;case"center":n.tableVAlign="center";break;case"end":n.tableVAlign="bottom";break}["left","center","right"].includes(t.alignItems)&&(n.tableAlign=t.alignItems);let s=t.margin.split(" ");s[0]&&s[0]==="auto"&&(n.tableAlign="center"),s[1]&&s[1]==="auto"&&(n.tableVAlign="center");let g=e=>{for(const r of this.supportedCSSProperties)if(new RegExp(r+"$").test(e))return!0;return!1},a=i.parentElement;return Object.keys(t).forEach(e=>{if(g(e))try{if(e.startsWith("font")){n[e]=t[e];return}let r=b(t[e],a);if(r&&a&&o&&((e.includes("width")||e.includes("left")||e.includes("right"))&&(r=r/a.getBoundingClientRect().width*100+"%"),(e.includes("height")||e.includes("top")||e.includes("bottom"))&&(r=r/a.getBoundingClientRect().height*100+"%")),e==="display"&&!t[e].includes("block"))return;typeof r=="number"&&(r+="px",r==="0px"&&(r="auto")),n[e]=r||t[e]}catch{console.log(e,t[e])}}),n}}class p{constructor(){this._excludeElementPattern=null,this.settings=m,this.cssParser=new w,this.img=new Image}excludeElementByPattern(i){return this._excludeElementPattern=new RegExp(i),this}applyCss(i,o={},t=[],n=[]){Object.keys(o).forEach(s=>{n.length>0&&!n.includes(s)||t.includes(s)||(i.style[s]=o[s])})}convert(i,o){if(this._excludeElementPattern&&(this._excludeElementPattern.test(i.className)||this._excludeElementPattern.test(i.id)))return null;let t={width:"",rows:{}},n=this.cssParser.parse(i);if(i.nodeType!==Element.TEXT_NODE){let g=Array.from(i.childNodes),a=Array.from(g).filter(r=>r.nodeType===Node.ELEMENT_NODE&&["DIV","SECTION","ARTICLE","MAIN","ASIDE","IMG"].includes(r.tagName)).length===0,e=0;g.forEach((r,c)=>{if(t.rows.hasOwnProperty(e)||(t.rows[e]={children:[],top:0,bottom:0}),r.nodeType===Element.TEXT_NODE||a){if(r.textContent.trim().length===0)return;let l=t.rows[e].children.length-1;if(l<0)t.rows[e].children.push(this.getCloneNode(r));else{let d=document.createElement("div"),h=t.rows[e].children[l];h.nodeType===Element.TEXT_NODE?d.innerHTML+=h.textContent:d.innerHTML+=h;let f=this.getCloneNode(r);f.nodeType===Element.TEXT_NODE?d.innerHTML+=f.textContent:d.appendChild(f),t.rows[e].children[l]=d.innerHTML}return}if(r.nodeType===Element.ELEMENT_NODE){let l=r.getBoundingClientRect();t.rows[e].top===0&&(t.rows[e].top=l.top),t.rows[e].bottom===0&&(t.rows[e].bottom=l.bottom),l.top>=t.rows[e].bottom&&(e++,t.rows[e]={children:[],top:l.top,bottom:l.bottom}),t.rows[e].top>l.top&&(t.rows[e].top=l.top),l.bottom>t.rows[e].bottom&&(t.rows[e].bottom=l.bottom);let d=this.settings.supportedHTMLTags.includes(r.tagName.toLowerCase())?this.getCloneNode(r):this.convert(r,i);if(!d)return;t.rows[e].children.push(d)}})}else i.textContent.trim().length>0&&t.rows[0].push(i.textContent);let s=this.createTable();return s.setAttribute("align",n.tableAlign??"left"),s.setAttribute("valign",n.tableVAlign??"top"),this.applyCss(s,n,["width"]),Object.keys(t.rows).forEach(g=>{let a=document.createElement("tr");t.rows[g].children.forEach((e,r)=>{let c=document.createElement("td");c.setAttribute("align",n.tableAlign??"left"),c.setAttribute("valign",n.tableVAlign??"top"),o&&(c.style.width=n.width,e.tagName!=="TABLE"&&e.getBoundingClientRect&&(c.style.width=e.getBoundingClientRect().width+"px")),typeof e=="string"?c.innerHTML=e:c.appendChild(e),a.appendChild(c)}),s.querySelector("tbody").appendChild(a)}),s}createTable(){let i=document.createElement("table");i.setAttribute("border",0),i.setAttribute("cellpadding",0),i.setAttribute("cellspacing",0);let o=document.createElement("tbody");return i.appendChild(o),i}convertSvgToImage(i){if(!i instanceof SVGElement)return i;let o=document.createElement("img"),t=new Image;const n=document.createElement("canvas"),s=new XMLSerializer().serializeToString(i);return t.onload=function(){n.width=i.getBoundingClientRect().width,n.height=i.getBoundingClientRect().height,n.getContext("2d").drawImage(t,0,0),o.src=n.toDataURL("image/png")},t.src="data:image/svg+xml;base64,"+btoa(s),o}getCloneNode(i){if(i instanceof SVGElement)return this.convertSvgToImage(i);let o=i.cloneNode(!1);if(i.nodeType===Node.ELEMENT_NODE){let n=this.cssParser.parse(i);Array.from(i.attributes).forEach(s=>{["href","src"].includes(s.name)||o.removeAttribute(s.name)}),Object.keys(n).forEach(s=>{o.style[s]=n[s]})}let t=i.childNodes;return Array.from(t).forEach(n=>{o.appendChild(this.getCloneNode(n))}),o}}module.exports=p;
228 changes: 228 additions & 0 deletions dist/html-to-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
const m = {
supportedCssProperties: [
"backgroundColor",
"background",
// Shorthand for multiple background properties
"backgroundImage",
"backgroundPosition",
"border(.)+",
// Shorthand for border-width, border-style, and border-color
"margin(.)+",
// Shorthand for all margin directions
"padding(.)+",
"width",
"height",
"maxWidth",
"maxHeight",
"color",
"fontSize",
"font",
"fontWeight",
"textAlign",
"textDecoration",
"-webkitTextDecorationColor",
// Prefix for text-decoration-color
"textIndent",
"textTransform",
"letterSpacing",
// 'display', // For 'block' or 'inline-block'
"listStyleType",
"listStylePosition"
],
supportedHTMLTags: [
"p",
"span",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"br",
"strong",
"em",
"blockquote",
"ul",
"li",
"ol",
"pre",
"a",
"image",
"img",
"svg",
"table",
"td",
"tr",
"tbody",
"code"
]
};
function b(u, i) {
let t = u.split(" ")[0];
const n = parseFloat(t);
if (isNaN(n))
return !1;
if (t.endsWith("px"))
return n;
if (t.endsWith("%"))
return !1;
if (t.endsWith("em") || t.endsWith("rem")) {
const s = parseFloat(getComputedStyle(i).fontSize);
return n * s;
} else if (t.endsWith("vw")) {
const s = window.innerWidth;
return n / 100 * s;
} else if (t.endsWith("vh")) {
const s = window.innerHeight;
return n / 100 * s;
} else if (t.endsWith("vmin")) {
const s = Math.min(window.innerWidth, window.innerHeight);
return n / 100 * s;
} else if (t.endsWith("vmax")) {
const s = Math.max(window.innerWidth, window.innerHeight);
return n / 100 * s;
} else
return n;
}
class w {
constructor() {
this.supportedCSSProperties = m.supportedCssProperties;
}
parse(i, o = !1) {
if (!i) return {};
let t = window.getComputedStyle(i), n = {};
switch (n.tableAlign = "left", t.textAlign !== "start" && (n.tableAlign = t.textAlign), n.tableVAlign = "top", t.justifyContent) {
case "start":
n.tableVAlign = "top";
break;
case "center":
n.tableVAlign = "center";
break;
case "end":
n.tableVAlign = "bottom";
break;
}
["left", "center", "right"].includes(t.alignItems) && (n.tableAlign = t.alignItems);
let s = t.margin.split(" ");
s[0] && s[0] === "auto" && (n.tableAlign = "center"), s[1] && s[1] === "auto" && (n.tableVAlign = "center");
let g = (e) => {
for (const r of this.supportedCSSProperties)
if (new RegExp(r + "$").test(e))
return !0;
return !1;
}, a = i.parentElement;
return Object.keys(t).forEach((e) => {
if (g(e))
try {
if (e.startsWith("font")) {
n[e] = t[e];
return;
}
let r = b(t[e], a);
if (r && a && o && ((e.includes("width") || e.includes("left") || e.includes("right")) && (r = r / a.getBoundingClientRect().width * 100 + "%"), (e.includes("height") || e.includes("top") || e.includes("bottom")) && (r = r / a.getBoundingClientRect().height * 100 + "%")), e === "display" && !t[e].includes("block"))
return;
typeof r == "number" && (r += "px", r === "0px" && (r = "auto")), n[e] = r || t[e];
} catch {
console.log(e, t[e]);
}
}), n;
}
}
class p {
constructor() {
this._excludeElementPattern = null, this.settings = m, this.cssParser = new w(), this.img = new Image();
}
excludeElementByPattern(i) {
return this._excludeElementPattern = new RegExp(i), this;
}
applyCss(i, o = {}, t = [], n = []) {
Object.keys(o).forEach((s) => {
n.length > 0 && !n.includes(s) || t.includes(s) || (i.style[s] = o[s]);
});
}
convert(i, o) {
if (this._excludeElementPattern && (this._excludeElementPattern.test(i.className) || this._excludeElementPattern.test(i.id)))
return null;
let t = {
width: "",
rows: {}
}, n = this.cssParser.parse(i);
if (i.nodeType !== Element.TEXT_NODE) {
let g = Array.from(i.childNodes), a = Array.from(g).filter((r) => r.nodeType === Node.ELEMENT_NODE && ["DIV", "SECTION", "ARTICLE", "MAIN", "ASIDE", "IMG"].includes(r.tagName)).length === 0, e = 0;
g.forEach((r, c) => {
if (t.rows.hasOwnProperty(e) || (t.rows[e] = {
children: [],
top: 0,
bottom: 0
}), r.nodeType === Element.TEXT_NODE || a) {
if (r.textContent.trim().length === 0)
return;
let l = t.rows[e].children.length - 1;
if (l < 0)
t.rows[e].children.push(this.getCloneNode(r));
else {
let d = document.createElement("div"), h = t.rows[e].children[l];
h.nodeType === Element.TEXT_NODE ? d.innerHTML += h.textContent : d.innerHTML += h;
let f = this.getCloneNode(r);
f.nodeType === Element.TEXT_NODE ? d.innerHTML += f.textContent : d.appendChild(f), t.rows[e].children[l] = d.innerHTML;
}
return;
}
if (r.nodeType === Element.ELEMENT_NODE) {
let l = r.getBoundingClientRect();
t.rows[e].top === 0 && (t.rows[e].top = l.top), t.rows[e].bottom === 0 && (t.rows[e].bottom = l.bottom), l.top >= t.rows[e].bottom && (e++, t.rows[e] = {
children: [],
top: l.top,
bottom: l.bottom
}), t.rows[e].top > l.top && (t.rows[e].top = l.top), l.bottom > t.rows[e].bottom && (t.rows[e].bottom = l.bottom);
let d = this.settings.supportedHTMLTags.includes(r.tagName.toLowerCase()) ? this.getCloneNode(r) : this.convert(r, i);
if (!d) return;
t.rows[e].children.push(d);
}
});
} else i.textContent.trim().length > 0 && t.rows[0].push(i.textContent);
let s = this.createTable();
return s.setAttribute("align", n.tableAlign ?? "left"), s.setAttribute("valign", n.tableVAlign ?? "top"), this.applyCss(s, n, ["width"]), Object.keys(t.rows).forEach((g) => {
let a = document.createElement("tr");
t.rows[g].children.forEach((e, r) => {
let c = document.createElement("td");
c.setAttribute("align", n.tableAlign ?? "left"), c.setAttribute("valign", n.tableVAlign ?? "top"), o && (c.style.width = n.width, e.tagName !== "TABLE" && e.getBoundingClientRect && (c.style.width = e.getBoundingClientRect().width + "px")), typeof e == "string" ? c.innerHTML = e : c.appendChild(e), a.appendChild(c);
}), s.querySelector("tbody").appendChild(a);
}), s;
}
createTable() {
let i = document.createElement("table");
i.setAttribute("border", 0), i.setAttribute("cellpadding", 0), i.setAttribute("cellspacing", 0);
let o = document.createElement("tbody");
return i.appendChild(o), i;
}
convertSvgToImage(i) {
if (!i instanceof SVGElement)
return i;
let o = document.createElement("img"), t = new Image();
const n = document.createElement("canvas"), s = new XMLSerializer().serializeToString(i);
return t.onload = function() {
n.width = i.getBoundingClientRect().width, n.height = i.getBoundingClientRect().height, n.getContext("2d").drawImage(t, 0, 0), o.src = n.toDataURL("image/png");
}, t.src = "data:image/svg+xml;base64," + btoa(s), o;
}
getCloneNode(i) {
if (i instanceof SVGElement)
return this.convertSvgToImage(i);
let o = i.cloneNode(!1);
if (i.nodeType === Node.ELEMENT_NODE) {
let n = this.cssParser.parse(i);
Array.from(i.attributes).forEach((s) => {
["href", "src"].includes(s.name) || o.removeAttribute(s.name);
}), Object.keys(n).forEach((s) => {
o.style[s] = n[s];
});
}
let t = i.childNodes;
return Array.from(t).forEach((n) => {
o.appendChild(this.getCloneNode(n));
}), o;
}
}
export {
p as default
};
28 changes: 27 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,36 @@
<h1>Convert HTML element to Email Compatible Layout</h1>
<textarea class="box" style="min-width: 50vw; min-height: 40vh;" placeholder="Enter Source Text">
</textarea>
<div id="output" style="width: 60vw;">
<div id="output" class="box" style="width: 50vw;">

</div>
<button>Convert</button>
<!--<div id="sample" style="width: 100vw; color:white;">-->
<!-- <body style="font-family: sans-serif;">-->

<!-- <h1 style="text-align: center; color: #333;">Welcome to My Shop</h1>-->

<!-- <div style="display: flex; justify-content: space-around; flex-wrap: wrap;">-->
<!-- Hello world-->
<!-- <div style="border: 1px solid #ccc; padding: 15px; margin: 10px; width: 280px; text-align: center;">-->
<!-- <img src="https://via.placeholder.com/250" alt="Product 1" style="max-width: 100%; height: auto;">-->
<!-- <h3 style="margin-top: 10px;">Product 1</h3>-->
<!-- <p style="color: #666;">This is a description of Product 1.</p>-->
<!-- <p style="font-weight: bold;">$19.99</p>-->
<!-- <button style="background-color: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer;">Add to Cart</button>-->
<!-- </div>-->

<!-- <div style="border: 1px solid #ccc; padding: 15px; margin: 10px; width: 280px; text-align: center;">-->
<!-- <img src="https://via.placeholder.com/250" alt="Product 2" style="max-width: 100%; height: auto;">-->
<!-- <h3 style="margin-top: 10px;">Product 2</h3>-->
<!-- <p style="color: #666;">This is a description of Product 2.</p>-->
<!-- <p style="font-weight: bold;">$24.99</p>-->
<!-- <button style="background-color: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer;">Add to Cart</button>-->
<!-- </div>-->

<!-- </div>-->
<!-- </body>-->
<!--</div>-->
<script type="module" src="./main.js"></script>
</body>
</html>
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import HTML2Table from "./src/HTML2Table.js";
import CssParser from "./src/CssParser.js";

export default HTML2Table;
13 changes: 9 additions & 4 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import HTML2Table from "./src/HTML2Table.js";

let w = new HTML2Table();
// document.querySelector('#test').appendChild(w);

document.querySelector('button').addEventListener('click',function(){
let placeholder = document.createElement('div');
Expand All @@ -10,8 +9,14 @@ document.querySelector('button').addEventListener('click',function(){
placeholder.style.color = '#f2f2f2'
placeholder.innerHTML = document.querySelector('textarea').value;
document.body.appendChild(placeholder);
document.getElementById('output').appendChild(w.convert(placeholder, {
document.getElementById('output').innerText = w.convert(placeholder, {
initialWidth: '100%'
}))
// placeholder.remove();
}).outerHTML
placeholder.remove();
});

// document.getElementById('output').appendChild(w.convert(document.querySelector('#sample'), {
// initialWidth: '100%'
// }))

// console.log(w.cssParser.parse(document.querySelector('#sample > div'), false))
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "html-to-table",
"version": "0.0.3",
"version": "0.1.0",
"description": "Convert HTML to Email Compatible Layout",
"main": "index.js",
"type": "module",
Expand Down
Loading

0 comments on commit 91ee4bd

Please sign in to comment.