Linux.do Export Markdown

Export Linux.do topics to Markdown with automatic flat, nest, and main-post-only modes.

2026-06-03 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Linux.do Export Markdown
// @name:zh-CN   Linux.do 帖子 Markdown 导出
// @name:en      Linux.do Export Markdown
// @namespace    https://github.com/kai-wei-kfuse/Linuxdo-Export-Markdown
// @version      1.0.0
// @description  Export Linux.do topics to Markdown with automatic flat, nest, and main-post-only modes.
// @description:zh-CN 将 Linux.do 论坛帖子导出为 Markdown,自动识别 flat/nest 模式,并支持只导出主帖或指定楼层。
// @description:en Export Linux.do topics to Markdown with automatic flat/nest detection, main-post-only export, and post range selection.
// @author       kai-wei-kfuse
// @license      MIT
// @homepageURL  https://github.com/kai-wei-kfuse/Linuxdo-Export-Markdown
// @supportURL   https://github.com/kai-wei-kfuse/Linuxdo-Export-Markdown/issues
// @match        https://linux.do/t/topic/*
// @match        https://linux.do/n/topic/*
// @match        https://www.linux.do/t/topic/*
// @match        https://www.linux.do/n/topic/*
// @grant        GM_download
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  const BUTTON_ID = "linuxdo-md-export-button";
  const DIALOG_ID = "linuxdo-md-export-dialog";
  const POST_ONLY_ALIASES = new Set(["post", "main", "主帖"]);

  function parseTopicLocation(urlText = location.href) {
    const url = new URL(urlText, location.origin);
    const parts = url.pathname.split("/").filter(Boolean);
    const viewToken = parts[0];
    let idToken = null;

    if (parts[1] === "topic" && /^\d+$/.test(parts[2] || "")) {
      idToken = parts[2];
    } else {
      idToken = parts.slice(1).find((part) => /^\d+$/.test(part));
    }

    if (!idToken || !["t", "n"].includes(viewToken)) {
      return null;
    }

    return {
      id: Number(idToken),
      viewToken,
      detectedMode: viewToken === "n" ? "nest" : "flat",
      canonicalJsonUrl: `${url.origin}/t/topic/${idToken}.json`,
      originalUrl: url.href.replace(/#.*$/, ""),
      origin: url.origin,
    };
  }

  function injectButton() {
    const topic = parseTopicLocation();
    if (!topic) return;

    const existing = document.getElementById(BUTTON_ID);
    if (existing) return;

    const button = document.createElement("button");
    button.id = BUTTON_ID;
    button.type = "button";
    button.textContent = "导出 MD";
    button.title = "导出当前 Linux.do 帖子为 Markdown";
    button.style.cssText = [
      "position:fixed",
      "right:18px",
      "bottom:82px",
      "z-index:99999",
      "border:1px solid #0f766e",
      "border-radius:8px",
      "background:#0d9488",
      "color:#fff",
      "font-size:14px",
      "font-weight:600",
      "line-height:1",
      "padding:10px 12px",
      "box-shadow:0 6px 18px rgba(15,23,42,.18)",
      "cursor:pointer",
    ].join(";");

    button.addEventListener("click", () => {
      exportCurrentTopic(button).catch((error) => {
        console.error("[linuxdo-md-export]", error);
        alert(`导出失败:${error.message || error}`);
      });
    });

    document.body.appendChild(button);
  }

  async function exportCurrentTopic(button) {
    const topicInfo = parseTopicLocation();
    if (!topicInfo) {
      throw new Error("当前页面不是可识别的 linux.do 帖子链接。");
    }

    const rangeInput = await showRangeDialog();

    if (rangeInput === null) return;

    const range = parseRange(rangeInput);
    const exportMode = range.kind === "post" ? "post" : topicInfo.detectedMode;

    setBusy(button, true);
    try {
      const topic = await fetchCompleteTopic(topicInfo);
      const selectedPosts = selectPosts(topic.posts, range);

      if (!selectedPosts.length) {
        throw new Error("所选范围内没有可导出的楼层。");
      }

      const markdown = buildMarkdown({
        topic,
        topicInfo,
        posts: selectedPosts,
        exportMode,
        range,
      });

      const filename = makeFilename(topicInfo.id, exportMode, topic.title);
      downloadText(filename, markdown);
    } finally {
      setBusy(button, false);
    }
  }

  function setBusy(button, busy) {
    button.disabled = busy;
    button.textContent = busy ? "导出中..." : "导出 MD";
    button.style.opacity = busy ? "0.72" : "1";
    button.style.cursor = busy ? "wait" : "pointer";
  }

  function showRangeDialog() {
    return new Promise((resolve) => {
      const existing = document.getElementById(DIALOG_ID);
      if (existing) existing.remove();

      const overlay = document.createElement("div");
      overlay.id = DIALOG_ID;
      overlay.style.cssText = [
        "position:fixed",
        "inset:0",
        "z-index:100000",
        "display:flex",
        "align-items:center",
        "justify-content:center",
        "background:rgba(15,23,42,.38)",
        "font-family:system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
      ].join(";");

      const dialog = document.createElement("div");
      dialog.setAttribute("role", "dialog");
      dialog.setAttribute("aria-modal", "true");
      dialog.setAttribute("aria-labelledby", "linuxdo-md-export-title");
      dialog.style.cssText = [
        "width:min(420px,calc(100vw - 32px))",
        "box-sizing:border-box",
        "border:1px solid rgba(15,23,42,.12)",
        "border-radius:8px",
        "background:#fff",
        "color:#0f172a",
        "box-shadow:0 18px 52px rgba(15,23,42,.28)",
        "padding:18px",
      ].join(";");

      const title = document.createElement("h2");
      title.id = "linuxdo-md-export-title";
      title.textContent = "选择导出范围";
      title.style.cssText = [
        "margin:0 0 14px",
        "font-size:18px",
        "font-weight:700",
        "line-height:1.3",
        "letter-spacing:0",
      ].join(";");

      const label = document.createElement("label");
      label.textContent = "范围";
      label.style.cssText = [
        "display:block",
        "margin:0 0 6px",
        "font-size:13px",
        "font-weight:600",
        "color:#334155",
      ].join(";");

      const select = document.createElement("select");
      select.style.cssText = [
        "width:100%",
        "box-sizing:border-box",
        "border:1px solid #cbd5e1",
        "border-radius:6px",
        "background:#fff",
        "color:#0f172a",
        "font-size:14px",
        "line-height:1.4",
        "padding:9px 10px",
        "outline:none",
      ].join(";");

      const options = [
        ["all", "全部回复"],
        ["post", "只导出主帖"],
        ["custom", "自定义楼层"],
      ];

      for (const [value, text] of options) {
        const option = document.createElement("option");
        option.value = value;
        option.textContent = text;
        select.appendChild(option);
      }

      const customWrap = document.createElement("div");
      customWrap.style.cssText = "display:none;margin-top:12px;";

      const customLabel = document.createElement("label");
      customLabel.textContent = "自定义楼层";
      customLabel.style.cssText = [
        "display:block",
        "margin:0 0 6px",
        "font-size:13px",
        "font-weight:600",
        "color:#334155",
      ].join(";");

      const customInput = document.createElement("input");
      customInput.type = "text";
      customInput.placeholder = "例如:1-50 或 1,3,8-12";
      customInput.style.cssText = [
        "width:100%",
        "box-sizing:border-box",
        "border:1px solid #cbd5e1",
        "border-radius:6px",
        "background:#fff",
        "color:#0f172a",
        "font-size:14px",
        "line-height:1.4",
        "padding:9px 10px",
        "outline:none",
      ].join(";");

      const error = document.createElement("div");
      error.setAttribute("aria-live", "polite");
      error.style.cssText = [
        "min-height:18px",
        "margin-top:10px",
        "font-size:13px",
        "line-height:1.4",
        "color:#b91c1c",
      ].join(";");

      const actions = document.createElement("div");
      actions.style.cssText = [
        "display:flex",
        "justify-content:flex-end",
        "gap:8px",
        "margin-top:16px",
      ].join(";");

      const cancelButton = document.createElement("button");
      cancelButton.type = "button";
      cancelButton.textContent = "取消";
      cancelButton.style.cssText = dialogButtonStyle("#fff", "#334155", "#cbd5e1");

      const confirmButton = document.createElement("button");
      confirmButton.type = "button";
      confirmButton.textContent = "导出";
      confirmButton.style.cssText = dialogButtonStyle("#0d9488", "#fff", "#0f766e");

      function close(value) {
        overlay.remove();
        document.removeEventListener("keydown", onKeydown);
        resolve(value);
      }

      function selectedRangeInput() {
        if (select.value === "custom") return customInput.value.trim();
        return select.value;
      }

      function syncCustomVisibility() {
        const show = select.value === "custom";
        customWrap.style.display = show ? "block" : "none";
        error.textContent = "";
        if (show) {
          setTimeout(() => customInput.focus(), 0);
        }
      }

      function confirm() {
        const value = selectedRangeInput();

        try {
          parseRange(value);
          close(value);
        } catch (parseError) {
          error.textContent = parseError.message || "范围格式无效。";
          customInput.focus();
        }
      }

      function onKeydown(event) {
        if (event.key === "Escape") {
          close(null);
        } else if (event.key === "Enter") {
          event.preventDefault();
          confirm();
        }
      }

      select.addEventListener("change", syncCustomVisibility);
      customInput.addEventListener("input", () => {
        error.textContent = "";
      });
      cancelButton.addEventListener("click", () => close(null));
      confirmButton.addEventListener("click", confirm);
      document.addEventListener("keydown", onKeydown);

      customWrap.appendChild(customLabel);
      customWrap.appendChild(customInput);
      actions.appendChild(cancelButton);
      actions.appendChild(confirmButton);
      dialog.appendChild(title);
      dialog.appendChild(label);
      dialog.appendChild(select);
      dialog.appendChild(customWrap);
      dialog.appendChild(error);
      dialog.appendChild(actions);
      overlay.appendChild(dialog);
      document.body.appendChild(overlay);

      select.focus();
    });
  }

  function dialogButtonStyle(background, color, borderColor) {
    return [
      `background:${background}`,
      `color:${color}`,
      `border:1px solid ${borderColor}`,
      "border-radius:6px",
      "font-size:14px",
      "font-weight:600",
      "line-height:1",
      "padding:9px 12px",
      "cursor:pointer",
    ].join(";");
  }

  function parseRange(input) {
    const raw = String(input || "").trim();
    const normalized = raw.toLowerCase();

    if (!raw || normalized === "all" || raw === "全部") {
      return { kind: "all", label: raw || "all", postNumbers: null };
    }

    if (POST_ONLY_ALIASES.has(normalized) || POST_ONLY_ALIASES.has(raw)) {
      return { kind: "post", label: raw, postNumbers: new Set([1]) };
    }

    const postNumbers = new Set();
    const segments = raw.split(",").map((part) => part.trim()).filter(Boolean);
    if (!segments.length) {
      throw new Error("范围格式为空。");
    }

    for (const segment of segments) {
      const rangeMatch = segment.match(/^(\d+)\s*-\s*(\d+)$/);
      const numberMatch = segment.match(/^\d+$/);

      if (rangeMatch) {
        const start = Number(rangeMatch[1]);
        const end = Number(rangeMatch[2]);
        if (start < 1 || end < 1 || start > end) {
          throw new Error(`范围无效:${segment}`);
        }
        for (let value = start; value <= end; value += 1) {
          postNumbers.add(value);
        }
      } else if (numberMatch) {
        const value = Number(segment);
        if (value < 1) throw new Error(`楼层无效:${segment}`);
        postNumbers.add(value);
      } else {
        throw new Error(`无法识别范围:${segment}`);
      }
    }

    return { kind: "range", label: raw, postNumbers };
  }

  async function fetchCompleteTopic(topicInfo) {
    const first = await fetchJson(topicInfo.canonicalJsonUrl);
    const postsById = new Map();
    const postsByNumber = new Map();

    for (const post of first?.post_stream?.posts || []) {
      if (post && post.id) postsById.set(post.id, post);
      if (post && post.post_number) postsByNumber.set(post.post_number, post);
    }

    const streamIds = Array.isArray(first?.post_stream?.stream) ? first.post_stream.stream : [];
    const missingIds = streamIds.filter((id) => !postsById.has(id));

    for (const chunk of chunkArray(missingIds, 50)) {
      const extraPosts = await fetchPostsByIds(topicInfo.id, chunk);
      for (const post of extraPosts) {
        if (post && post.id) postsById.set(post.id, post);
        if (post && post.post_number) postsByNumber.set(post.post_number, post);
      }
    }

    return {
      id: topicInfo.id,
      title: first?.title || document.title.replace(/\s*-\s*LINUX DO\s*$/i, "").trim() || `topic-${topicInfo.id}`,
      category: first?.category_id,
      tags: Array.isArray(first?.tags) ? first.tags : [],
      posts: [...postsByNumber.values()].sort((a, b) => a.post_number - b.post_number),
      raw: first,
    };
  }

  async function fetchPostsByIds(topicId, ids) {
    if (!ids.length) return [];

    const params = new URLSearchParams();
    for (const id of ids) params.append("post_ids[]", String(id));

    const url = `${location.origin}/t/${topicId}/posts.json?${params.toString()}`;
    const data = await fetchJson(url);
    return data?.post_stream?.posts || data?.posts || [];
  }

  async function fetchJson(url) {
    const response = await fetch(url, {
      credentials: "same-origin",
      headers: {
        Accept: "application/json",
      },
    });

    if (!response.ok) {
      throw new Error(`请求失败 ${response.status}:${url}`);
    }

    return response.json();
  }

  function selectPosts(posts, range) {
    const selected = posts.filter((post) => {
      if (!post || !post.post_number) return false;
      if (!range.postNumbers) return true;
      return range.postNumbers.has(post.post_number);
    });

    return selected.sort((a, b) => a.post_number - b.post_number);
  }

  function buildMarkdown({ topic, topicInfo, posts, exportMode, range }) {
    const skipped = [];
    const visiblePosts = [];

    for (const post of posts) {
      const body = htmlToMarkdown(post.cooked || "").trim();
      if (!body) {
        skipped.push(post.post_number);
      } else {
        visiblePosts.push({ post, body });
      }
    }

    const lines = [
      `# ${escapeMarkdownLine(topic.title)}`,
      "",
      `- 原始链接: ${topicInfo.originalUrl}`,
      `- 导出模式: ${exportMode}`,
      `- 导出时间: ${new Date().toLocaleString()}`,
      `- 楼层范围: ${range.label || "all"}`,
      "",
      "---",
      "",
    ];

    if (exportMode === "nest") {
      lines.push(renderNestedPosts(visiblePosts, topicInfo));
    } else {
      lines.push(renderFlatPosts(visiblePosts, topicInfo));
    }

    if (skipped.length) {
      lines.push("", "---", "", `跳过空白/不可见楼层: ${skipped.join(", ")}`, "");
    }

    return normalizeMarkdown(lines.join("\n"));
  }

  function renderFlatPosts(items, topicInfo) {
    return items.map(({ post, body }) => {
      return [
        `## #${post.post_number} ${formatAuthor(post)}`,
        "",
        renderPostMeta(post, topicInfo),
        "",
        body,
        "",
      ].join("\n");
    }).join("\n");
  }

  function renderNestedPosts(items, topicInfo) {
    const byNumber = new Map(items.map((item) => [item.post.post_number, { ...item, children: [] }]));
    const roots = [];
    const outOfRangeParents = [];

    const mainPostNode = byNumber.get(1);

    for (const node of byNumber.values()) {
      const parentNumber = Number(node.post.reply_to_post_number || 0);
      if (parentNumber && byNumber.has(parentNumber)) {
        byNumber.get(parentNumber).children.push(node);
      } else if (parentNumber && !byNumber.has(parentNumber)) {
        outOfRangeParents.push(node);
      } else if (mainPostNode && node.post.post_number !== 1) {
        mainPostNode.children.push(node);
      } else {
        roots.push(node);
      }
    }

    for (const node of byNumber.values()) {
      node.children.sort((a, b) => a.post.post_number - b.post.post_number);
    }

    roots.sort((a, b) => a.post.post_number - b.post.post_number);
    outOfRangeParents.sort((a, b) => a.post.post_number - b.post.post_number);

    const lines = [];
    for (const root of roots) {
      renderNestedNode(root, topicInfo, 2, lines);
    }

    if (outOfRangeParents.length) {
      lines.push("## 范围外父级回复", "");
      for (const node of outOfRangeParents) {
        renderNestedNode(node, topicInfo, 3, lines);
      }
    }

    return lines.join("\n");
  }

  function renderNestedNode(node, topicInfo, depth, lines) {
    const level = Math.min(depth, 6);
    lines.push(`${"#".repeat(level)} #${node.post.post_number} ${formatAuthor(node.post)}`);
    lines.push("");
    lines.push(renderPostMeta(node.post, topicInfo));
    lines.push("");
    lines.push(node.body);
    lines.push("");

    for (const child of node.children) {
      renderNestedNode(child, topicInfo, depth + 1, lines);
    }
  }

  function renderPostMeta(post, topicInfo) {
    const parts = [
      `作者: ${formatAuthor(post)}`,
      `时间: ${formatDate(post.created_at)}`,
      `链接: ${makePostUrl(topicInfo, post.post_number)}`,
    ];

    if (post.reply_to_post_number) {
      parts.push(`回复: #${post.reply_to_post_number}`);
    }

    return parts.map((part) => `- ${part}`).join("\n");
  }

  function formatAuthor(post) {
    const display = post.display_username || post.name || post.username || "unknown";
    return post.username && post.username !== display ? `${display} (@${post.username})` : display;
  }

  function formatDate(value) {
    if (!value) return "";
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
  }

  function makePostUrl(topicInfo, postNumber) {
    return `${topicInfo.origin}/${topicInfo.viewToken}/topic/${topicInfo.id}/${postNumber}`;
  }

  function htmlToMarkdown(html) {
    const doc = new DOMParser().parseFromString(`<div>${html || ""}</div>`, "text/html");
    const root = doc.body.firstElementChild;
    return normalizeMarkdown(renderChildren(root, { listDepth: 0 }).trim());
  }

  function renderChildren(node, context) {
    return [...node.childNodes].map((child) => renderNode(child, context)).join("");
  }

  function renderNode(node, context) {
    if (node.nodeType === Node.TEXT_NODE) {
      return node.nodeValue.replace(/\s+/g, " ");
    }

    if (node.nodeType !== Node.ELEMENT_NODE) {
      return "";
    }

    const tag = node.tagName.toLowerCase();
    const text = () => renderChildren(node, context).trim();
    const block = (content) => `\n\n${content.trim()}\n\n`;

    switch (tag) {
      case "br":
        return "\n";
      case "p":
        return block(text());
      case "strong":
      case "b":
        return `**${text()}**`;
      case "em":
      case "i":
        return `*${text()}*`;
      case "code":
        if (node.closest("pre")) return node.textContent || "";
        return `\`${(node.textContent || "").replace(/`/g, "\\`")}\``;
      case "pre": {
        const code = node.textContent.replace(/\n+$/, "");
        return `\n\n\`\`\`\n${code}\n\`\`\`\n\n`;
      }
      case "blockquote": {
        const content = normalizeMarkdown(text());
        return block(content.split("\n").map((line) => `> ${line}`.trimEnd()).join("\n"));
      }
      case "a": {
        const href = node.getAttribute("href");
        const label = text() || href || "";
        if (!href) return label;
        return `[${escapeMarkdownLinkText(label)}](${absoluteUrl(href)})`;
      }
      case "img": {
        const src = node.getAttribute("src");
        if (!src) return "";
        const alt = node.getAttribute("alt") || node.getAttribute("title") || "image";
        return `![${escapeMarkdownLinkText(alt)}](${absoluteUrl(src)})`;
      }
      case "ul":
      case "ol":
        return renderList(node, context, tag === "ol");
      case "li":
        return text();
      case "h1":
      case "h2":
      case "h3":
      case "h4":
      case "h5":
      case "h6": {
        const level = Number(tag.slice(1)) + 1;
        return block(`${"#".repeat(Math.min(level, 6))} ${text()}`);
      }
      case "div":
      case "section":
      case "article":
      case "aside":
        return block(text());
      case "span":
        return text();
      default:
        return text();
    }
  }

  function renderList(node, context, ordered) {
    const depth = context.listDepth || 0;
    const lines = [];
    let index = 1;

    for (const child of [...node.children].filter((element) => element.tagName.toLowerCase() === "li")) {
      const marker = ordered ? `${index}.` : "-";
      const prefix = "  ".repeat(depth) + marker + " ";
      const rendered = renderChildren(child, { ...context, listDepth: depth + 1 }).trim();
      const [firstLine, ...restLines] = rendered.split("\n");
      lines.push(prefix + firstLine);
      for (const line of restLines) {
        lines.push("  ".repeat(depth + 1) + line);
      }
      index += 1;
    }

    return `\n${lines.join("\n")}\n`;
  }

  function absoluteUrl(value) {
    try {
      return new URL(value, location.origin).href;
    } catch {
      return value;
    }
  }

  function escapeMarkdownLine(value) {
    return String(value || "").replace(/\s+/g, " ").trim();
  }

  function escapeMarkdownLinkText(value) {
    return String(value || "").replace(/]/g, "\\]").replace(/\s+/g, " ").trim();
  }

  function normalizeMarkdown(markdown) {
    return markdown
      .replace(/[ \t]+\n/g, "\n")
      .replace(/\n{4,}/g, "\n\n\n")
      .trim()
      + "\n";
  }

  function makeFilename(topicId, mode, title) {
    const safeTitle = String(title || "topic")
      .replace(/[\\/:*?"<>|]/g, " ")
      .replace(/\s+/g, "-")
      .replace(/^-+|-+$/g, "")
      .slice(0, 80) || "topic";

    return `linuxdo-${topicId}-${mode}-${safeTitle}.md`;
  }

  function downloadText(filename, text) {
    const blob = new Blob([text], { type: "text/markdown;charset=utf-8" });
    const url = URL.createObjectURL(blob);

    if (typeof GM_download === "function") {
      GM_download({
        url,
        name: filename,
        saveAs: true,
        ontimeout: () => fallbackDownload(url, filename),
        onerror: () => fallbackDownload(url, filename),
        onload: () => setTimeout(() => URL.revokeObjectURL(url), 3000),
      });
      return;
    }

    fallbackDownload(url, filename);
  }

  function fallbackDownload(url, filename) {
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    link.remove();
    setTimeout(() => URL.revokeObjectURL(url), 3000);
  }

  function chunkArray(values, size) {
    const chunks = [];
    for (let index = 0; index < values.length; index += size) {
      chunks.push(values.slice(index, index + size));
    }
    return chunks;
  }

  injectButton();

  let previousUrl = location.href;
  setInterval(() => {
    if (location.href !== previousUrl) {
      previousUrl = location.href;
      const existing = document.getElementById(BUTTON_ID);
      if (existing) existing.remove();
      injectButton();
    }
  }, 1000);
})();