diff --git a/gallery_dl/extractor/deviantart.py b/gallery_dl/extractor/deviantart.py
index aec14cb6..22a74a02 100644
--- a/gallery_dl/extractor/deviantart.py
+++ b/gallery_dl/extractor/deviantart.py
@@ -402,7 +402,7 @@ class DeviantartExtractor(Extractor):
if html["type"] == "tiptap":
try:
- return self._tiptap_to_html(markup)
+ return self.utils("tiptap").to_html(markup)
except Exception as exc:
self.log.traceback(exc)
self.log.error("%s: '%s: %s'", deviation["index"],
@@ -411,242 +411,6 @@ class DeviantartExtractor(Extractor):
self.log.warning("%s: Unsupported '%s' markup.",
deviation["index"], html["type"])
- def _tiptap_to_html(self, markup):
- html = []
-
- html.append('
')
- data = util.json_loads(markup)
- for block in data["document"]["content"]:
- self._tiptap_process_content(html, block)
- html.append("
")
-
- return "".join(html)
-
- def _tiptap_process_content(self, html, content):
- type = content["type"]
-
- if type == "paragraph":
- if children := content.get("content"):
- html.append('')
- else:
- html.append('margin-inline-start:0px">')
-
- for block in children:
- self._tiptap_process_content(html, block)
- html.append("
")
- else:
- html.append('
')
-
- elif type == "text":
- self._tiptap_process_text(html, content)
-
- elif type == "heading":
- attrs = content["attrs"]
- level = str(attrs.get("level") or "3")
-
- html.append("')
- html.append('')
- self._tiptap_process_children(html, content)
- html.append("")
-
- elif type in ("listItem", "bulletList", "orderedList", "blockquote"):
- c = type[1]
- tag = (
- "li" if c == "i" else
- "ul" if c == "u" else
- "ol" if c == "r" else
- "blockquote"
- )
- html.append("<" + tag + ">")
- self._tiptap_process_children(html, content)
- html.append("" + tag + ">")
-
- elif type == "anchor":
- attrs = content["attrs"]
- html.append('')
-
- elif type == "hardBreak":
- html.append("
")
-
- elif type == "horizontalRule":
- html.append("
")
-
- elif type == "da-deviation":
- self._tiptap_process_deviation(html, content)
-
- elif type == "da-mention":
- user = content["attrs"]["user"]["username"]
- html.append('@')
- html.append(user)
- html.append('')
-
- elif type == "da-gif":
- attrs = content["attrs"]
- width = str(attrs.get("width") or "")
- height = str(attrs.get("height") or "")
- url = text.escape(attrs.get("url") or "")
-
- html.append('')
-
- elif type == "da-video":
- src = text.escape(content["attrs"].get("src") or "")
- html.append('')
-
- else:
- self.log.warning("Unsupported content type '%s'", type)
-
- def _tiptap_process_text(self, html, content):
- if marks := content.get("marks"):
- close = []
- for mark in marks:
- type = mark["type"]
- if type == "link":
- attrs = mark.get("attrs") or {}
- html.append('')
- close.append("")
- elif type == "bold":
- html.append("")
- close.append("")
- elif type == "italic":
- html.append("")
- close.append("")
- elif type == "underline":
- html.append("")
- close.append("")
- elif type == "strike":
- html.append("")
- close.append("")
- elif type == "textStyle" and len(mark) <= 1:
- pass
- else:
- self.log.warning("Unsupported text marker '%s'", type)
- close.reverse()
- html.append(text.escape(content["text"]))
- html.extend(close)
- else:
- html.append(text.escape(content["text"]))
-
- def _tiptap_process_children(self, html, content):
- if children := content.get("content"):
- for block in children:
- self._tiptap_process_content(html, block)
-
- def _tiptap_process_indentation(self, html, attrs):
- itype = ("text-indent" if attrs.get("indentType") == "line" else
- "margin-inline-start")
- isize = str((attrs.get("indentation") or 0) * 24)
- html.append(itype + ":" + isize + "px")
-
- def _tiptap_process_deviation(self, html, content):
- dev = content["attrs"]["deviation"]
- media = dev.get("media") or ()
-
- html.append('')
- html.append('
')
-
- if "baseUri" in media:
- url, formats = self._eclipse_media(media)
- full = formats["fullview"]
-
- html.append('')
-
- html.append('
')
- html.append("")
-
- elif "textContent" in dev:
- html.append('')
-
- html.append(' ')
-
def _extract_content(self, deviation):
content = deviation["content"]
@@ -827,25 +591,6 @@ x2="45.4107524%" y2="71.4898596%" id="app-root-3">\
self.log.info("Unwatching %s", username)
self.api.user_friends_unwatch(username)
- def _eclipse_media(self, media, format="preview"):
- url = [media["baseUri"]]
-
- formats = {
- fmt["t"]: fmt
- for fmt in media["types"]
- }
-
- if tokens := media.get("token") or ():
- if len(tokens) <= 1:
- fmt = formats[format]
- if "c" in fmt:
- url.append(fmt["c"].replace(
- "", media["prettyName"]))
- url.append("?token=")
- url.append(tokens[-1])
-
- return "".join(url), formats
-
def _eclipse_to_oauth(self, eclipse_api, deviations):
for obj in deviations:
deviation = obj["deviation"] if "deviation" in obj else obj
@@ -1303,7 +1048,7 @@ class DeviantartDeviationExtractor(DeviantartExtractor):
yield deviation
for index, post in enumerate(additional_media):
- uri = self._eclipse_media(post["media"], "fullview")[0]
+ uri = eclipse_media(post["media"], "fullview")[0]
deviation["content"]["src"] = uri
deviation["num"] += 1
deviation["index_file"] = post["fileId"]
@@ -2245,3 +1990,23 @@ by {username}, {date}
{content}
"""
+
+
+def eclipse_media(media, format="preview"):
+ url = [media["baseUri"]]
+
+ formats = {
+ fmt["t"]: fmt
+ for fmt in media["types"]
+ }
+
+ if tokens := media.get("token") or ():
+ if len(tokens) <= 1:
+ fmt = formats[format]
+ if "c" in fmt:
+ url.append(fmt["c"].replace(
+ "", media["prettyName"]))
+ url.append("?token=")
+ url.append(tokens[-1])
+
+ return "".join(url), formats
diff --git a/gallery_dl/extractor/utils/deviantart_tiptap.py b/gallery_dl/extractor/utils/deviantart_tiptap.py
new file mode 100644
index 00000000..36dfc6af
--- /dev/null
+++ b/gallery_dl/extractor/utils/deviantart_tiptap.py
@@ -0,0 +1,256 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2026 Mike Fährmann
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+
+from ... import text, util
+from .. deviantart import eclipse_media
+
+
+def to_html(markup):
+ html = []
+
+ html.append('')
+ data = util.json_loads(markup)
+ for block in data["document"]["content"]:
+ process_content(html, block)
+ html.append("
")
+
+ return "".join(html)
+
+
+def process_content(html, content):
+ type = content["type"]
+
+ if type == "paragraph":
+ if children := content.get("content"):
+ html.append('')
+ else:
+ html.append('margin-inline-start:0px">')
+
+ for block in children:
+ process_content(html, block)
+ html.append("
")
+ else:
+ html.append('
')
+
+ elif type == "text":
+ process_text(html, content)
+
+ elif type == "heading":
+ attrs = content["attrs"]
+ level = str(attrs.get("level") or "3")
+
+ html.append("')
+ html.append('')
+ process_children(html, content)
+ html.append("")
+
+ elif type in ("listItem", "bulletList", "orderedList", "blockquote"):
+ c = type[1]
+ tag = (
+ "li" if c == "i" else
+ "ul" if c == "u" else
+ "ol" if c == "r" else
+ "blockquote"
+ )
+ html.append("<" + tag + ">")
+ process_children(html, content)
+ html.append("" + tag + ">")
+
+ elif type == "anchor":
+ attrs = content["attrs"]
+ html.append('')
+
+ elif type == "hardBreak":
+ html.append("
")
+
+ elif type == "horizontalRule":
+ html.append("
")
+
+ elif type == "da-deviation":
+ process_deviation(html, content)
+
+ elif type == "da-mention":
+ user = content["attrs"]["user"]["username"]
+ html.append('@')
+ html.append(user)
+ html.append('')
+
+ elif type == "da-gif":
+ attrs = content["attrs"]
+ width = str(attrs.get("width") or "")
+ height = str(attrs.get("height") or "")
+ url = text.escape(attrs.get("url") or "")
+
+ html.append('')
+
+ elif type == "da-video":
+ src = text.escape(content["attrs"].get("src") or "")
+ html.append('')
+
+ else:
+ import logging
+ logging.getLogger("tiptap").warning(
+ "Unsupported content type '%s'", type)
+
+
+def process_text(html, content):
+ if marks := content.get("marks"):
+ close = []
+ for mark in marks:
+ type = mark["type"]
+ if type == "link":
+ attrs = mark.get("attrs") or {}
+ html.append('')
+ close.append("")
+ elif type == "bold":
+ html.append("")
+ close.append("")
+ elif type == "italic":
+ html.append("")
+ close.append("")
+ elif type == "underline":
+ html.append("")
+ close.append("")
+ elif type == "strike":
+ html.append("")
+ close.append("")
+ elif type == "textStyle" and len(mark) <= 1:
+ pass
+ else:
+ import logging
+ logging.getLogger("tiptap").warning(
+ "Unsupported text marker '%s'", type)
+ close.reverse()
+ html.append(text.escape(content["text"]))
+ html.extend(close)
+ else:
+ html.append(text.escape(content["text"]))
+
+
+def process_children(html, content):
+ if children := content.get("content"):
+ for block in children:
+ process_content(html, block)
+
+
+def process_indentation(html, attrs):
+ itype = ("text-indent" if attrs.get("indentType") == "line" else
+ "margin-inline-start")
+ isize = str((attrs.get("indentation") or 0) * 24)
+ html.append(itype + ":" + isize + "px")
+
+
+def process_deviation(html, content):
+ dev = content["attrs"]["deviation"]
+ media = dev.get("media") or ()
+
+ html.append('')
+ html.append('
')
+
+ if "baseUri" in media:
+ url, formats = eclipse_media(media)
+ full = formats["fullview"]
+
+ html.append('')
+
+ html.append('
')
+ html.append("")
+
+ elif "textContent" in dev:
+ html.append('')
+
+ html.append(' ')