3c57c167f2
导入 SiteForge Factory V5 基线源码: - FastAPI + SQLite + Docker Compose(Web + Worker)架构 - 四套商业主题 × 14 页面模板(1.1.0,含历史版本快照) - 四种建站模式骨架 + 迁移审计 + 发布门禁 - 52 项 pytest 测试 - 补全计划主文档 docs/superpowers/plans/ Co-Authored-By: Claude Code <noreply@anthropic.com>
84 lines
4.4 KiB
Python
84 lines
4.4 KiB
Python
"""Conservative WordPress WXR adapter helpers.
|
|
|
|
Never execute PHP, Elementor widgets, shortcodes or serialized metadata.
|
|
Only known text/image widgets are converted to sanitized HTML with warnings
|
|
for unknown proprietary elements; original metadata is always retained.
|
|
"""
|
|
import json, re
|
|
from html import escape
|
|
from .core import sanitize
|
|
|
|
WIDGETS={'heading','text-editor','image','button','icon-box','image-box','divider','spacer','video','html'}
|
|
|
|
def elementor_to_html(raw):
|
|
if not isinstance(raw,str) or not raw.strip():return {'html':'','warnings':[],'widgets':0}
|
|
try:data=json.loads(raw)
|
|
except (ValueError,TypeError):return {'html':'','warnings':['Elementor JSON 解析失败:保留原始字段,需人工检查'],'widgets':0}
|
|
if not isinstance(data,list):return {'html':'','warnings':['Elementor 数据根节点不是数组,需人工检查'],'widgets':0}
|
|
parts=[];warnings=[];count=[0]
|
|
def walk(elements,depth=0):
|
|
if depth>12: warnings.append('Elementor 嵌套超过安全限制');return
|
|
if not isinstance(elements,list):return
|
|
for item in elements[:300]:
|
|
if not isinstance(item,dict):continue
|
|
typ=item.get('widgetType') or ''
|
|
settings=item.get('settings') or {}
|
|
if not isinstance(settings,dict):settings={}
|
|
if typ:
|
|
count[0]+=1
|
|
if typ not in WIDGETS:
|
|
if len(warnings)<60:warnings.append('未转换的 Elementor 组件:'+str(typ)[:65])
|
|
elif typ=='heading':
|
|
size=settings.get('header_size','h2');size=size if size in ('h1','h2','h3','h4','h5','h6') else 'h2'
|
|
parts.append(f'<{size}>{escape(str(settings.get("title") or ""))}</{size}>')
|
|
elif typ=='text-editor':
|
|
parts.append(sanitize(str(settings.get('editor') or '')))
|
|
elif typ in ('image','image-box'):
|
|
image=settings.get('image') or {}
|
|
url=image.get('url','') if isinstance(image,dict) else ''
|
|
if isinstance(url,str) and url.startswith(('https://','http://','/')):
|
|
parts.append(f'<p><img src="{escape(url,quote=True)}" alt="{escape(str(settings.get("title") or ""),quote=True)}"></p>')
|
|
if typ=='image-box' and settings.get('title_text'):
|
|
parts.append('<h3>'+escape(str(settings['title_text']))+'</h3>')
|
|
if typ=='image-box' and settings.get('description_text'):
|
|
parts.append('<p>'+escape(str(settings['description_text']))+'</p>')
|
|
elif typ=='button':
|
|
href=settings.get('link') or {}
|
|
dest=href.get('url','') if isinstance(href,dict) else ''
|
|
if isinstance(dest,str) and dest.startswith(('https://','http://','/')):
|
|
parts.append(f'<a href="{escape(dest,quote=True)}">{escape(str(settings.get("text") or "更多信息"))}</a>')
|
|
elif typ=='html':
|
|
parts.append(sanitize(str(settings.get('html') or '')))
|
|
elif typ in ('video','divider','spacer','icon-box'):
|
|
if len(warnings)<60:warnings.append(f'{typ} 布局/嵌入需人工复核')
|
|
walk(item.get('elements') or [],depth+1)
|
|
walk(data)
|
|
return {'html':'\n'.join(parts),'warnings':warnings,'widgets':count[0]}
|
|
|
|
def wp_taxonomy(item):
|
|
data={}
|
|
for node in item:
|
|
if node.tag.rsplit('}',1)[-1]!='category':continue
|
|
domain=node.get('domain','')
|
|
name=(node.text or '').strip()
|
|
if not name:continue
|
|
if domain in ('category','product_cat','vehicle_cat'):data['category']=name
|
|
elif domain in ('product_brand','brand','pa_brand'):data['brand']=name
|
|
elif domain=='product_type':data['product_type']=name
|
|
elif domain.startswith('pa_'):data[domain[3:]]=name
|
|
return data
|
|
|
|
def wp_media_map(root):
|
|
"""WXR attachment ID -> original URL, useful for featured and gallery images."""
|
|
media={}
|
|
for item in root.findall('./channel/item'):
|
|
typ=post_id=url=''
|
|
for node in item:
|
|
local=node.tag.rsplit('}',1)[-1]
|
|
if local=='post_type':typ=node.text or ''
|
|
elif local=='post_id':post_id=node.text or ''
|
|
elif local=='attachment_url':url=node.text or ''
|
|
if typ=='attachment' and post_id and url and url.startswith(('http://','https://')):
|
|
media[post_id]=url
|
|
return media
|