"""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'
'+escape(str(settings['description_text']))+'
') 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'{escape(str(settings.get("text") or "更多信息"))}') 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