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