Files
cms-SiteForge/app/wordpress_compat.py
T
浅念 8cea7dee20 chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests)
- Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR
  imports keep post/product types instead of collapsing to page
- Docs: trade-pro theme port spec (R3) and implementation plan

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 16:49:25 +08:00

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