diff --git a/css/json-converter.css b/css/json-converter.css
new file mode 100644
index 0000000..ed745a7
--- /dev/null
+++ b/css/json-converter.css
@@ -0,0 +1,53 @@
+.status {
+ margin-top: 16px;
+ padding: 8px 16px;
+ border-radius: 4px;
+ display: none;
+ }
+
+ #fileInputWrapper {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: auto;
+ height: auto;
+ margin-bottom: 16px;
+ cursor: pointer;
+ }
+
+ #fileInput {
+ display: none;
+ }
+
+ #fileInputLabel {
+ gap: 2px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 9999px;
+ padding: 16px 24px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ outline: none;
+ background-color: #16a34a;
+ color: #ffffff;
+ }
+
+ #fileInputLabel:hover {
+ background-color: #15803d;
+ color: #ffffff;
+ }
+
+ #fileInputLabel:active {
+ background-color: #15803d;
+ color: #f3f4f6;
+ }
+
+ #fileInputLabel:focus-visible {
+ outline: 2px solid #16a34a;
+ outline-offset: 2px;
+ }
+
+ #dropZone.dragover {
+ background-color: #f3f4f6;
+ }
\ No newline at end of file
diff --git a/css/styles.css b/css/styles.css
index bd6213e..a90f074 100644
--- a/css/styles.css
+++ b/css/styles.css
@@ -1,3 +1,4 @@
@tailwind base;
@tailwind components;
-@tailwind utilities;
\ No newline at end of file
+@tailwind utilities;
+
diff --git a/css/tailwind.css b/css/tailwind.css
index c05ef62..87dbdb1 100644
--- a/css/tailwind.css
+++ b/css/tailwind.css
@@ -1055,6 +1055,14 @@ select {
margin-bottom: -1.5rem;
}
+.-ml-2 {
+ margin-left: -0.5rem;
+}
+
+.mb-4 {
+ margin-bottom: 1rem;
+}
+
.mb-\[-1\%\] {
margin-bottom: -1%;
}
@@ -1067,6 +1075,10 @@ select {
margin-left: 1rem;
}
+.mr-4 {
+ margin-right: 1rem;
+}
+
.mt-0 {
margin-top: 0px;
}
@@ -1127,6 +1139,14 @@ select {
aspect-ratio: 1155/678;
}
+.h-1 {
+ height: 0.25rem;
+}
+
+.h-12 {
+ height: 3rem;
+}
+
.h-24 {
height: 6rem;
}
@@ -1155,6 +1175,18 @@ select {
height: 100%;
}
+.h-px {
+ height: 1px;
+}
+
+.w-1 {
+ width: 0.25rem;
+}
+
+.w-12 {
+ width: 3rem;
+}
+
.w-5 {
width: 1.25rem;
}
@@ -1179,10 +1211,18 @@ select {
width: 100%;
}
+.w-screen {
+ width: 100vw;
+}
+
.max-w-2xl {
max-width: 42rem;
}
+.max-w-3xl {
+ max-width: 48rem;
+}
+
.max-w-4xl {
max-width: 56rem;
}
@@ -1203,11 +1243,20 @@ select {
flex: 1 1 auto;
}
+.flex-none {
+ flex: none;
+}
+
.-translate-x-1\/2 {
--tw-translate-x: -50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
+.-translate-x-full {
+ --tw-translate-x: -100%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
.rotate-\[30deg\] {
--tw-rotate: 30deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1262,6 +1311,10 @@ select {
gap: 1.5rem;
}
+.gap-8 {
+ gap: 2rem;
+}
+
.gap-x-3 {
-moz-column-gap: 0.75rem;
column-gap: 0.75rem;
@@ -1325,6 +1378,10 @@ select {
border-radius: 0.25rem;
}
+.rounded-2xl {
+ border-radius: 1rem;
+}
+
.rounded-full {
border-radius: 9999px;
}
@@ -1349,6 +1406,11 @@ select {
border-bottom-width: 2px;
}
+.border-gray-100 {
+ --tw-border-opacity: 1;
+ border-color: rgb(243 244 246 / var(--tw-border-opacity));
+}
+
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@@ -1373,6 +1435,10 @@ select {
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
+.bg-gray-900\/10 {
+ background-color: rgb(17 24 39 / 0.1);
+}
+
.bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
@@ -1383,6 +1449,10 @@ select {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
+.bg-white\/50 {
+ background-color: rgb(255 255 255 / 0.5);
+}
+
.bg-gradient-to-t {
background-image: linear-gradient(to top, var(--tw-gradient-stops));
}
@@ -1427,10 +1497,18 @@ select {
padding: 0.625rem;
}
+.p-4 {
+ padding: 1rem;
+}
+
.p-6 {
padding: 1.5rem;
}
+.p-8 {
+ padding: 2rem;
+}
+
.px-0 {
padding-left: 0px;
padding-right: 0px;
@@ -1830,6 +1908,10 @@ select {
left: calc(50% - 30rem);
}
+ .sm\:-ml-4 {
+ margin-left: -1rem;
+ }
+
.sm\:mt-20 {
margin-top: 5rem;
}
@@ -1860,6 +1942,10 @@ select {
border-radius: 1.5rem;
}
+ .sm\:p-16 {
+ padding: 4rem;
+ }
+
.sm\:px-16 {
padding-left: 4rem;
padding-right: 4rem;
@@ -1912,6 +1998,23 @@ select {
}
@media (min-width: 1024px) {
+ .lg\:static {
+ position: static;
+ }
+
+ .lg\:mx-0 {
+ margin-left: 0px;
+ margin-right: 0px;
+ }
+
+ .lg\:-mr-6 {
+ margin-right: -1.5rem;
+ }
+
+ .lg\:ml-8 {
+ margin-left: 2rem;
+ }
+
.lg\:mt-24 {
margin-top: 6rem;
}
@@ -1924,6 +2027,10 @@ select {
display: none;
}
+ .lg\:w-auto {
+ width: auto;
+ }
+
.lg\:max-w-none {
max-width: none;
}
@@ -1932,10 +2039,23 @@ select {
flex: 1 1 0%;
}
+ .lg\:flex-auto {
+ flex: 1 1 auto;
+ }
+
+ .lg\:translate-x-0 {
+ --tw-translate-x: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+ }
+
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
+ .lg\:grid-cols-4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+
.lg\:justify-end {
justify-content: flex-end;
}
@@ -1953,4 +2073,5 @@ select {
.lg\:text-center {
text-align: center;
}
-}
\ No newline at end of file
+}
+
diff --git a/index.html b/index.html
index 18942b3..64c7419 100644
--- a/index.html
+++ b/index.html
@@ -64,8 +64,8 @@
class="-mx-3 flex rounded-full px-6 py-2 text-base font-semibold leading-7 text-gray-900 hover:bg-black/5 items-center gap-1">Github
- Roadmap
+ Json Converter
diff --git a/js/json-converter.js b/js/json-converter.js
new file mode 100644
index 0000000..ec54088
--- /dev/null
+++ b/js/json-converter.js
@@ -0,0 +1,221 @@
+// 扩展 String 对象,添加 remove 方法
+String.prototype.remove = function (toRemove) {
+ if (Array.isArray(toRemove)) {
+ return toRemove.reduce((acc, value) => acc.replace(value, ''), this);
+ }
+ if (typeof toRemove === 'string') {
+ return this.replace(toRemove, '');
+ }
+ return this;
+};
+
+// 清理对象,删除值为 undefined 的属性
+const cleanupObject = (obj) => {
+ Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {}));
+ return obj;
+};
+
+// 判断是否为文件夹
+const isFolder = (item) => !!item.match(/.*<\/H3>/);
+
+// 判断是否为链接
+const isLink = (item) => !!item.match(/.*<\/A>/);
+
+// 获取标题
+const getTitle = (item) => item.match(/<(H3|A).*>(.*)<\/(H3|A)>/)?.[2];
+
+// 获取图标
+const getIcon = (item) => item.match(/ICON="(.+)"/)?.[1];
+
+// 获取URL
+const getUrl = (item) => item.match(/HREF="([^"]*)"/)?.[1];
+
+// 获取数值属性
+const getNumericProperty = (item, property) => {
+ const match = item.match(new RegExp(`${property}="([\\d]+)"`));
+ return match ? parseInt(match[1]) : undefined;
+};
+
+// 转换链接为对象
+const transformLink = (markup) =>
+ cleanupObject({
+ type: 'link',
+ addDate: getNumericProperty(markup, 'ADD_DATE'),
+ title: getTitle(markup),
+ icon: getIcon(markup),
+ url: getUrl(markup),
+ });
+
+// 转换文件夹为对象
+const transformFolder = (markup) =>
+ cleanupObject({
+ type: 'folder',
+ addDate: getNumericProperty(markup, 'ADD_DATE'),
+ lastModified: getNumericProperty(markup, 'LAST_MODIFIED'),
+ title: getTitle(markup),
+ });
+
+// 查找指定缩进级别的项目
+const findItemsAtIndentLevel = (markup, level) =>
+ markup.match(new RegExp(`^\\s{${level * 4}}(.*)[\r\n]`, 'gm'));
+
+// 查找指定缩进级别的链接
+const findLinks = (markup, level) => findItemsAtIndentLevel(markup, level).filter(isLink);
+
+// 查找指定缩进级别的文件夹
+const findFolders = (markup, level) => {
+ const folders = findItemsAtIndentLevel(markup, level);
+ return folders?.map((folder, index) => {
+ const isLastOne = index === folders.length - 1;
+ return markup.substring(
+ markup.indexOf(folder),
+ isLastOne ? undefined : markup.indexOf(folders[index + 1]),
+ );
+ });
+};
+
+// 查找子项目
+const findChildren = (markup, level = 1) => {
+ if (findItemsAtIndentLevel(markup, level)) {
+ const links = findLinks(markup, level);
+ const folders = findFolders(markup.remove(links), level);
+ return [...(links || []), ...(folders || [])];
+ }
+};
+
+// 处理子项目
+const processChild = (child, level = 1) => {
+ if (isFolder(child)) return processFolder(child, level);
+ if (isLink(child)) return transformLink(child);
+};
+
+// 处理文件夹及其子项目
+const processFolder = (folder, level) => {
+ const children = findChildren(folder, level + 1);
+ return cleanupObject({
+ ...transformFolder(folder),
+ children: children?.map((child) => processChild(child, level + 1))?.filter(Boolean),
+ });
+};
+
+// 将书签转换为JSON格式
+const bookmarksToJSON = (markup, { stringify = true, formatJSON = false, spaces = 2 } = {}) => {
+ const obj = findChildren(markup)?.map(child => processChild(child));
+ if (!stringify) return obj;
+ return JSON.stringify(obj, ...(formatJSON ? [null, spaces] : []));
+};
+
+// 处理文件输入和拖放上传的交互逻辑
+const fileInput = document.getElementById('fileInput');
+const fileNameDisplay = document.getElementById('fileName');
+const fileSizeDisplay = document.getElementById('fileSize');
+const dropZone = document.getElementById('dropZone');
+const uploadButton = document.getElementById('uploadButton');
+const statusIndicator = document.getElementById('status');
+const fileDetails = document.getElementById('fileDetails');
+
+// 根据文件大小选择合适的单位
+const formatFileSize = (size) => {
+ return size > 1024 ? `${(size / 1024).toFixed(2)} MB` : `${size.toFixed(2)} KB`;
+};
+
+fileInput.addEventListener('change', (event) => {
+ const file = event.target.files[0];
+ const fileName = file ? file.name : 'Choose file';
+ fileNameDisplay.textContent = fileName;
+ if (file) {
+ if (file.type !== 'text/html') {
+ alert('Please re-upload the bookmarks file in html format');
+ fileInput.value = ''; // 清空文件输入
+ fileNameDisplay.textContent = 'Choose file';
+ fileSizeDisplay.style.display = 'none';
+ uploadButton.style.display = 'none';
+ fileDetails.style.display = 'none';
+ return;
+ }
+ fileSizeDisplay.textContent = `Size: ${formatFileSize(file.size / 1024)}`;
+ fileSizeDisplay.style.display = 'block';
+ uploadButton.style.display = 'inline-flex';
+ statusIndicator.style.display = 'none'; // 隐藏状态指示器
+ fileDetails.style.display = 'flex'; // 显示文件详细信息
+ } else {
+ fileSizeDisplay.style.display = 'none';
+ uploadButton.style.display = 'none';
+ fileDetails.style.display = 'none';
+ }
+ uploadButton.textContent = 'Convert';
+});
+
+dropZone.addEventListener('dragover', (event) => {
+ event.preventDefault();
+ dropZone.classList.add('dragover');
+});
+
+dropZone.addEventListener('dragleave', () => {
+ dropZone.classList.remove('dragover');
+});
+
+dropZone.addEventListener('drop', (event) => {
+ event.preventDefault();
+ dropZone.classList.remove('dragover');
+ const files = event.dataTransfer.files;
+ if (files.length) {
+ fileInput.files = files;
+ const file = files[0];
+ const fileName = file.name;
+ fileNameDisplay.textContent = fileName;
+ if (file.type !== 'text/html') {
+ alert('Please re-upload the bookmarks file in html format');
+ fileInput.value = ''; // 清空文件输入
+ fileNameDisplay.textContent = 'Choose file';
+ fileSizeDisplay.style.display = 'none';
+ uploadButton.style.display = 'none';
+ fileDetails.style.display = 'none';
+ return;
+ }
+ fileSizeDisplay.textContent = `Size: ${formatFileSize(file.size / 1024)}`;
+ fileSizeDisplay.style.display = 'block';
+ uploadButton.style.display = 'inline-flex';
+ statusIndicator.style.display = 'none'; // 隐藏状态指示器
+ fileDetails.style.display = 'flex'; // 显示文件详细信息
+ uploadButton.textContent = 'Convert';
+ }
+});
+
+uploadButton.addEventListener('click', async () => {
+ if (!fileInput.files.length) {
+ alert('Please select a file first.');
+ return;
+ }
+
+ uploadButton.style.display = 'none';
+ statusIndicator.className = 'group gap-2 inline-flex items-center justify-center rounded-full py-2 px-6 text-sm font-semibold text-green-600';
+ statusIndicator.textContent = 'Uploading...';
+ statusIndicator.style.display = 'inline-flex';
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟文件上传
+
+ statusIndicator.className = 'group gap-2 inline-flex items-center justify-center rounded-full py-2 px-6 text-sm font-semibold text-green-600';
+ statusIndicator.textContent = 'Uploaded 100%';
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟文件处理
+
+ statusIndicator.className = 'group gap-2 inline-flex items-center justify-center rounded-full py-2 px-6 text-sm font-semibold text-green-600';
+ statusIndicator.innerHTML = `
+
+ Initializing...`;
+
+ const file = fileInput.files[0];
+ const text = await file.text();
+
+ const json = bookmarksToJSON(text, { stringify: true, formatJSON: true, spaces: 2 });
+
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const jsonSize = formatFileSize(blob.size / 1024); // 计算JSON文件大小
+
+ statusIndicator.className = 'group gap-2 inline-flex items-center justify-center rounded-full py-2 px-6 text-sm font-semibold focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 bg-green-600 text-white hover:text-white hover:bg-green-700 active:bg-green-700 active:text-green-100 focus-visible:outline-green-600';
+ statusIndicator.innerHTML = `Download JSON`;
+ fileNameDisplay.textContent = 'pintree.json';
+ fileSizeDisplay.textContent = `Size: ${jsonSize}`;
+});
\ No newline at end of file
diff --git a/json-converter.html b/json-converter.html
new file mode 100644
index 0000000..c899c47
--- /dev/null
+++ b/json-converter.html
@@ -0,0 +1,322 @@
+
+
+
+
+
+
+ Pintree
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Bookmark Converter
+
Turn your browser bookmarks into beautiful
+ navigation sites in minutes!
+
+
+
+
Upload your bookmark file
+
Start by uploading your HTML bookmark file.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file