Skip to content

错误处理

HTML Layout Parser 提供了完善的错误处理机制,帮助您诊断和处理各种解析问题。

错误类型

错误代码分类

代码范围类别描述
0成功操作成功完成
1xxx输入验证错误HTML、CSS 或参数验证失败
2xxx字体相关错误字体加载、卸载或使用错误
3xxx解析错误HTML/CSS 解析过程中的错误
4xxx内存错误内存分配或管理错误
5xxx内部错误系统内部错误

常见错误代码

typescript
// 输入验证错误 (1xxx)
const INPUT_ERRORS = {
  1001: 'HTML 内容为空',
  1002: '视口宽度无效',
  1003: 'CSS 语法错误',
  1004: '参数类型错误',
  1005: '文档大小超过限制'
};

// 字体错误 (2xxx)
const FONT_ERRORS = {
  2001: '字体数据无效',
  2002: '字体加载失败',
  2003: '字体 ID 不存在',
  2004: '字体格式不支持',
  2005: '默认字体未设置'
};

// 解析错误 (3xxx)
const PARSE_ERRORS = {
  3001: 'HTML 解析失败',
  3002: 'CSS 解析失败',
  3003: '布局计算失败',
  3004: '解析超时',
  3005: '不支持的 CSS 属性'
};

// 内存错误 (4xxx)
const MEMORY_ERRORS = {
  4001: '内存分配失败',
  4002: '内存使用超过限制',
  4003: '缓冲区溢出',
  4004: '内存泄漏检测到'
};

// 内部错误 (5xxx)
const INTERNAL_ERRORS = {
  5001: 'WASM 模块未初始化',
  5002: '内部状态错误',
  5003: '系统资源不足',
  5004: '未知内部错误'
};

基本错误处理

使用 try-catch

typescript
async function basicErrorHandling() {
  const parser = new HtmlLayoutParser();
  
  try {
    await parser.init();
    
    const fontData = await fetch('/fonts/arial.ttf').then(r => r.arrayBuffer());
    const fontId = parser.loadFont(new Uint8Array(fontData), 'Arial');
    
    if (fontId <= 0) {
      throw new Error('字体加载失败');
    }
    
    parser.setDefaultFont(fontId);
    
    const layouts = parser.parse(html, { viewportWidth: 800 });
    return layouts;
    
  } catch (error) {
    console.error('解析过程中出错:', error);
    
    // 根据错误类型进行处理
    if (error.message.includes('字体')) {
      console.error('字体相关错误,请检查字体文件');
    } else if (error.message.includes('HTML')) {
      console.error('HTML 解析错误,请检查 HTML 格式');
    } else {
      console.error('未知错误:', error);
    }
    
    return null;
  } finally {
    parser.destroy();
  }
}

使用诊断模式

typescript
function diagnosticErrorHandling(html: string) {
  const parser = new HtmlLayoutParser();
  
  try {
    // 使用诊断模式获取详细错误信息
    const result = parser.parseWithDiagnostics(html, {
      viewportWidth: 800,
      enableMetrics: true
    });
    
    if (result.success) {
      console.log('解析成功');
      
      // 检查警告
      if (result.warnings && result.warnings.length > 0) {
        console.warn('解析警告:');
        result.warnings.forEach(warning => {
          console.warn(`  - ${warning.message}`);
          if (warning.line) {
            console.warn(`    位置: 第 ${warning.line} 行, 第 ${warning.column} 列`);
          }
        });
      }
      
      return result.data;
    } else {
      console.error('解析失败');
      
      // 处理错误
      if (result.errors) {
        result.errors.forEach(error => {
          console.error(`错误 [${error.code}]: ${error.message}`);
          
          if (error.line && error.column) {
            console.error(`  位置: 第 ${error.line} 行, 第 ${error.column} 列`);
          }
          
          if (error.suggestion) {
            console.error(`  建议: ${error.suggestion}`);
          }
        });
      }
      
      return null;
    }
  } finally {
    parser.destroy();
  }
}

高级错误处理

错误分类处理器

typescript
class ErrorHandler {
  private parser: HtmlLayoutParser;
  private retryCount: Map<string, number> = new Map();
  private maxRetries: number = 3;

  constructor(parser: HtmlLayoutParser, maxRetries: number = 3) {
    this.parser = parser;
    this.maxRetries = maxRetries;
  }

  async handleParseWithRetry(
    html: string, 
    options: { viewportWidth: number },
    retryKey?: string
  ): Promise<CharLayout[] | null> {
    const key = retryKey || this.generateRetryKey(html, options);
    const currentRetries = this.retryCount.get(key) || 0;

    try {
      const result = this.parser.parseWithDiagnostics(html, options);
      
      if (result.success) {
        // 成功时重置重试计数
        this.retryCount.delete(key);
        return result.data;
      } else {
        return this.handleParseErrors(result.errors || [], html, options, key);
      }
    } catch (error) {
      return this.handleException(error, html, options, key);
    }
  }

  private async handleParseErrors(
    errors: any[],
    html: string,
    options: { viewportWidth: number },
    retryKey: string
  ): Promise<CharLayout[] | null> {
    for (const error of errors) {
      const errorCode = error.code;
      
      switch (Math.floor(errorCode / 1000)) {
        case 1: // 输入验证错误
          return this.handleInputError(error, html, options);
          
        case 2: // 字体错误
          const fontFixed = await this.handleFontError(error);
          if (fontFixed) {
            return this.retryParse(html, options, retryKey);
          }
          break;
          
        case 3: // 解析错误
          const parseFixed = this.handleParseError(error, html);
          if (parseFixed) {
            return this.retryParse(parseFixed, options, retryKey);
          }
          break;
          
        case 4: // 内存错误
          const memoryFixed = await this.handleMemoryError(error);
          if (memoryFixed) {
            return this.retryParse(html, options, retryKey);
          }
          break;
          
        case 5: // 内部错误
          console.error('内部错误,无法自动修复:', error);
          break;
      }
    }
    
    return null;
  }

  private handleInputError(error: any, html: string, options: { viewportWidth: number }): CharLayout[] | null {
    switch (error.code) {
      case 1001: // HTML 内容为空
        console.warn('HTML 内容为空,返回空结果');
        return [];
        
      case 1002: // 视口宽度无效
        console.warn('视口宽度无效,使用默认值 800');
        return this.parser.parse(html, { ...options, viewportWidth: 800 });
        
      case 1005: // 文档大小超过限制
        console.warn('文档过大,尝试截断处理');
        const truncatedHtml = html.substring(0, 50000);
        return this.parser.parse(truncatedHtml, options);
        
      default:
        console.error('无法处理的输入错误:', error);
        return null;
    }
  }

  private async handleFontError(error: any): Promise<boolean> {
    switch (error.code) {
      case 2005: // 默认字体未设置
        console.warn('默认字体未设置,尝试加载系统字体');
        try {
          // 尝试加载一个基本字体
          const response = await fetch('/fonts/arial.ttf');
          if (response.ok) {
            const fontData = new Uint8Array(await response.arrayBuffer());
            const fontId = this.parser.loadFont(fontData, 'Arial');
            if (fontId > 0) {
              this.parser.setDefaultFont(fontId);
              return true;
            }
          }
        } catch (e) {
          console.error('加载默认字体失败:', e);
        }
        return false;
        
      case 2001: // 字体数据无效
      case 2002: // 字体加载失败
        console.warn('字体问题,清理并重新加载字体');
        this.parser.clearAllFonts();
        // 这里可以尝试重新加载字体
        return false;
        
      default:
        return false;
    }
  }

  private handleParseError(error: any, html: string): string | null {
    switch (error.code) {
      case 3004: // 解析超时
        console.warn('解析超时,尝试简化 HTML');
        // 移除复杂的 CSS
        return html.replace(/style\s*=\s*"[^"]*"/g, '');
        
      case 3005: // 不支持的 CSS 属性
        console.warn('包含不支持的 CSS,尝试清理');
        // 移除所有内联样式
        return html.replace(/style\s*=\s*"[^"]*"/g, '');
        
      default:
        return null;
    }
  }

  private async handleMemoryError(error: any): Promise<boolean> {
    switch (error.code) {
      case 4002: // 内存使用超过限制
        console.warn('内存使用过高,清理缓存和字体');
        this.parser.clearCache();
        
        // 只保留一个默认字体
        const fonts = this.parser.getLoadedFonts();
        if (fonts.length > 1) {
          for (let i = 1; i < fonts.length; i++) {
            this.parser.unloadFont(fonts[i].id);
          }
        }
        return true;
        
      default:
        return false;
    }
  }

  private async retryParse(
    html: string,
    options: { viewportWidth: number },
    retryKey: string
  ): Promise<CharLayout[] | null> {
    const currentRetries = this.retryCount.get(retryKey) || 0;
    
    if (currentRetries >= this.maxRetries) {
      console.error(`重试次数已达上限 (${this.maxRetries}),放弃解析`);
      return null;
    }
    
    this.retryCount.set(retryKey, currentRetries + 1);
    console.log(`第 ${currentRetries + 1} 次重试解析...`);
    
    // 等待一小段时间再重试
    await new Promise(resolve => setTimeout(resolve, 100 * (currentRetries + 1)));
    
    return this.handleParseWithRetry(html, options, retryKey);
  }

  private handleException(
    error: any,
    html: string,
    options: { viewportWidth: number },
    retryKey: string
  ): Promise<CharLayout[] | null> {
    console.error('解析过程中发生异常:', error);
    
    // 检查是否是内存相关的异常
    if (error.message.includes('memory') || error.message.includes('allocation')) {
      console.warn('检测到内存相关异常,尝试清理内存');
      this.parser.clearCache();
      this.parser.clearAllFonts();
      
      return this.retryParse(html, options, retryKey);
    }
    
    // 检查是否是初始化相关的异常
    if (error.message.includes('not initialized')) {
      console.warn('解析器未初始化,尝试重新初始化');
      // 这里需要外部重新初始化解析器
      return Promise.resolve(null);
    }
    
    return Promise.resolve(null);
  }

  private generateRetryKey(html: string, options: { viewportWidth: number }): string {
    // 生成基于内容和选项的唯一键
    const contentHash = this.simpleHash(html);
    return `${contentHash}-${options.viewportWidth}`;
  }

  private simpleHash(str: string): string {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 转换为 32 位整数
    }
    return hash.toString();
  }

  clearRetryHistory(): void {
    this.retryCount.clear();
  }
}

// 使用示例
const parser = new HtmlLayoutParser();
await parser.init();

const errorHandler = new ErrorHandler(parser, 3);

const layouts = await errorHandler.handleParseWithRetry(html, {
  viewportWidth: 800
});

if (layouts) {
  console.log('解析成功,获得', layouts.length, '个字符布局');
} else {
  console.error('解析最终失败');
}

错误恢复策略

typescript
class ErrorRecoveryManager {
  private parser: HtmlLayoutParser;
  private fallbackStrategies: Array<(html: string, options: any) => Promise<CharLayout[] | null>>;

  constructor(parser: HtmlLayoutParser) {
    this.parser = parser;
    this.fallbackStrategies = [
      this.strategySimplifyCSS.bind(this),
      this.strategyRemoveComplexElements.bind(this),
      this.strategyBasicTextOnly.bind(this),
      this.strategyEmergencyFallback.bind(this)
    ];
  }

  async parseWithRecovery(html: string, options: { viewportWidth: number }): Promise<{
    success: boolean;
    data: CharLayout[] | null;
    strategy: string;
    attempts: number;
  }> {
    // 首先尝试正常解析
    try {
      const result = this.parser.parseWithDiagnostics(html, options);
      if (result.success) {
        return {
          success: true,
          data: result.data,
          strategy: 'normal',
          attempts: 1
        };
      }
    } catch (error) {
      console.warn('正常解析失败,开始错误恢复:', error.message);
    }

    // 依次尝试恢复策略
    for (let i = 0; i < this.fallbackStrategies.length; i++) {
      const strategy = this.fallbackStrategies[i];
      const strategyName = this.getStrategyName(i);
      
      console.log(`尝试恢复策略: ${strategyName}`);
      
      try {
        const result = await strategy(html, options);
        if (result && result.length > 0) {
          return {
            success: true,
            data: result,
            strategy: strategyName,
            attempts: i + 2
          };
        }
      } catch (error) {
        console.warn(`恢复策略 ${strategyName} 失败:`, error.message);
      }
    }

    return {
      success: false,
      data: null,
      strategy: 'all_failed',
      attempts: this.fallbackStrategies.length + 1
    };
  }

  private async strategySimplifyCSS(html: string, options: any): Promise<CharLayout[] | null> {
    // 策略 1: 简化 CSS
    const simplifiedHtml = html
      .replace(/style\s*=\s*"[^"]*"/g, '') // 移除内联样式
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') // 移除 style 标签
      .replace(/class\s*=\s*"[^"]*"/g, ''); // 移除 class 属性

    return this.parser.parse(simplifiedHtml, options);
  }

  private async strategyRemoveComplexElements(html: string, options: any): Promise<CharLayout[] | null> {
    // 策略 2: 移除复杂元素
    const simplifiedHtml = html
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // 移除 script
      .replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '') // 移除 iframe
      .replace(/<object[^>]*>[\s\S]*?<\/object>/gi, '') // 移除 object
      .replace(/<embed[^>]*>/gi, '') // 移除 embed
      .replace(/<canvas[^>]*>[\s\S]*?<\/canvas>/gi, '') // 移除 canvas
      .replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, ''); // 移除 svg

    return this.parser.parse(simplifiedHtml, options);
  }

  private async strategyBasicTextOnly(html: string, options: any): Promise<CharLayout[] | null> {
    // 策略 3: 只保留基本文本元素
    const textOnlyHtml = html
      .replace(/<(?!\/?(div|p|span|h[1-6]|br|strong|em|b|i)\b)[^>]*>/gi, '') // 只保留基本文本标签
      .replace(/\s+/g, ' ') // 合并空白字符
      .trim();

    if (!textOnlyHtml) {
      return null;
    }

    return this.parser.parse(`<div>${textOnlyHtml}</div>`, options);
  }

  private async strategyEmergencyFallback(html: string, options: any): Promise<CharLayout[] | null> {
    // 策略 4: 紧急回退 - 提取纯文本
    const textContent = html
      .replace(/<[^>]*>/g, ' ') // 移除所有标签
      .replace(/\s+/g, ' ') // 合并空白字符
      .trim();

    if (!textContent) {
      return [];
    }

    // 创建最简单的 HTML
    const emergencyHtml = `<div>${textContent}</div>`;
    
    try {
      return this.parser.parse(emergencyHtml, {
        ...options,
        maxCharacters: Math.min(textContent.length, 1000) // 限制长度
      });
    } catch (error) {
      // 最后的回退:返回空数组而不是失败
      console.warn('所有恢复策略都失败,返回空结果');
      return [];
    }
  }

  private getStrategyName(index: number): string {
    const names = [
      'simplify_css',
      'remove_complex_elements',
      'basic_text_only',
      'emergency_fallback'
    ];
    return names[index] || 'unknown';
  }
}

// 使用示例
const parser = new HtmlLayoutParser();
await parser.init();

const recoveryManager = new ErrorRecoveryManager(parser);

const result = await recoveryManager.parseWithRecovery(html, {
  viewportWidth: 800
});

if (result.success) {
  console.log(`解析成功 (策略: ${result.strategy}, 尝试次数: ${result.attempts})`);
  console.log('字符数:', result.data?.length);
} else {
  console.error('所有恢复策略都失败了');
}

错误监控和报告

错误统计收集器

typescript
class ErrorStatistics {
  private stats: Map<number, {
    count: number;
    lastOccurrence: number;
    samples: string[];
  }> = new Map();

  recordError(errorCode: number, context?: string): void {
    const existing = this.stats.get(errorCode) || {
      count: 0,
      lastOccurrence: 0,
      samples: []
    };

    existing.count++;
    existing.lastOccurrence = Date.now();
    
    if (context && existing.samples.length < 5) {
      existing.samples.push(context);
    }

    this.stats.set(errorCode, existing);
  }

  getErrorReport(): {
    totalErrors: number;
    errorsByCategory: Record<string, number>;
    topErrors: Array<{ code: number; count: number; category: string }>;
    recentErrors: Array<{ code: number; timestamp: number }>;
  } {
    const totalErrors = Array.from(this.stats.values())
      .reduce((sum, stat) => sum + stat.count, 0);

    const errorsByCategory: Record<string, number> = {};
    const topErrors: Array<{ code: number; count: number; category: string }> = [];
    const recentErrors: Array<{ code: number; timestamp: number }> = [];

    for (const [code, stat] of this.stats) {
      const category = this.getCategoryName(code);
      errorsByCategory[category] = (errorsByCategory[category] || 0) + stat.count;
      
      topErrors.push({ code, count: stat.count, category });
      
      if (Date.now() - stat.lastOccurrence < 3600000) { // 最近 1 小时
        recentErrors.push({ code, timestamp: stat.lastOccurrence });
      }
    }

    topErrors.sort((a, b) => b.count - a.count);
    recentErrors.sort((a, b) => b.timestamp - a.timestamp);

    return {
      totalErrors,
      errorsByCategory,
      topErrors: topErrors.slice(0, 10),
      recentErrors: recentErrors.slice(0, 20)
    };
  }

  private getCategoryName(errorCode: number): string {
    const category = Math.floor(errorCode / 1000);
    const categories: Record<number, string> = {
      1: '输入验证',
      2: '字体相关',
      3: '解析错误',
      4: '内存错误',
      5: '内部错误'
    };
    return categories[category] || '未知';
  }

  clear(): void {
    this.stats.clear();
  }

  exportStats(): string {
    return JSON.stringify(Object.fromEntries(this.stats), null, 2);
  }
}

// 使用示例
const errorStats = new ErrorStatistics();

// 在错误处理中记录
try {
  const layouts = parser.parse(html, { viewportWidth: 800 });
} catch (error) {
  // 假设我们能从错误中提取错误代码
  const errorCode = extractErrorCode(error);
  errorStats.recordError(errorCode, html.substring(0, 100));
}

// 定期生成报告
setInterval(() => {
  const report = errorStats.getErrorReport();
  console.log('错误统计报告:', report);
}, 300000); // 每 5 分钟

错误上报系统

typescript
class ErrorReporter {
  private endpoint: string;
  private apiKey: string;
  private batchSize: number;
  private errorQueue: Array<{
    timestamp: number;
    errorCode: number;
    message: string;
    context: any;
    userAgent: string;
    sessionId: string;
  }> = [];

  constructor(endpoint: string, apiKey: string, batchSize: number = 10) {
    this.endpoint = endpoint;
    this.apiKey = apiKey;
    this.batchSize = batchSize;
  }

  reportError(
    errorCode: number,
    message: string,
    context: any = {},
    sessionId: string = 'unknown'
  ): void {
    this.errorQueue.push({
      timestamp: Date.now(),
      errorCode,
      message,
      context,
      userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'Node.js',
      sessionId
    });

    if (this.errorQueue.length >= this.batchSize) {
      this.flushErrors();
    }
  }

  private async flushErrors(): Promise<void> {
    if (this.errorQueue.length === 0) return;

    const errors = [...this.errorQueue];
    this.errorQueue = [];

    try {
      await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.apiKey}`
        },
        body: JSON.stringify({
          errors,
          timestamp: Date.now(),
          version: '0.0.1'
        })
      });
    } catch (error) {
      console.warn('错误上报失败:', error);
      // 将错误重新加入队列
      this.errorQueue.unshift(...errors);
    }
  }

  async flush(): Promise<void> {
    await this.flushErrors();
  }
}

// 使用示例
const errorReporter = new ErrorReporter(
  'https://api.example.com/errors',
  'your-api-key',
  5
);

// 在错误处理中使用
try {
  const layouts = parser.parse(html, { viewportWidth: 800 });
} catch (error) {
  const errorCode = extractErrorCode(error);
  
  errorReporter.reportError(
    errorCode,
    error.message,
    {
      htmlLength: html.length,
      viewportWidth: 800,
      stackTrace: error.stack
    },
    generateSessionId()
  );
  
  // 继续处理错误...
}

// 应用关闭时确保所有错误都已上报
process.on('beforeExit', async () => {
  await errorReporter.flush();
});

生产环境错误处理

优雅降级

typescript
class GracefulDegradation {
  private parser: HtmlLayoutParser;
  private fallbackRenderer: (html: string) => CharLayout[];

  constructor(parser: HtmlLayoutParser) {
    this.parser = parser;
    this.fallbackRenderer = this.createFallbackRenderer();
  }

  async parseWithGracefulDegradation(
    html: string,
    options: { viewportWidth: number }
  ): Promise<{
    layouts: CharLayout[];
    degraded: boolean;
    reason?: string;
  }> {
    try {
      // 尝试正常解析
      const layouts = this.parser.parse(html, options);
      return {
        layouts,
        degraded: false
      };
    } catch (error) {
      console.warn('正常解析失败,启用优雅降级:', error.message);
      
      try {
        // 尝试简化解析
        const simplifiedLayouts = await this.trySimplifiedParse(html, options);
        if (simplifiedLayouts) {
          return {
            layouts: simplifiedLayouts,
            degraded: true,
            reason: 'simplified_parse'
          };
        }
      } catch (simplifiedError) {
        console.warn('简化解析也失败:', simplifiedError.message);
      }

      // 最后的回退:使用纯文本渲染
      const fallbackLayouts = this.fallbackRenderer(html);
      return {
        layouts: fallbackLayouts,
        degraded: true,
        reason: 'fallback_renderer'
      };
    }
  }

  private async trySimplifiedParse(
    html: string,
    options: { viewportWidth: number }
  ): Promise<CharLayout[] | null> {
    // 移除复杂的 CSS 和元素
    const simplifiedHtml = html
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
      .replace(/style\s*=\s*"[^"]*"/g, '')
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
      .replace(/<(?!\/?(div|p|span|h[1-6]|br|strong|em|b|i|a)\b)[^>]*>/gi, '');

    return this.parser.parse(simplifiedHtml, {
      ...options,
      maxCharacters: 10000,
      timeout: 5000
    });
  }

  private createFallbackRenderer(): (html: string) => CharLayout[] {
    return (html: string) => {
      // 提取纯文本
      const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
      
      // 创建基本的字符布局
      const layouts: CharLayout[] = [];
      let x = 0;
      const y = 20; // 基线位置
      const fontSize = 16;
      const charWidth = fontSize * 0.6; // 估算字符宽度

      for (let i = 0; i < text.length; i++) {
        const char = text[i];
        
        if (char === ' ') {
          x += charWidth;
          continue;
        }

        layouts.push({
          character: char,
          x,
          y,
          width: charWidth,
          height: fontSize,
          baseline: y,
          fontFamily: 'Arial',
          fontSize,
          fontWeight: 400,
          fontStyle: 'normal',
          fontId: 0,
          color: '#000000FF',
          backgroundColor: '#00000000',
          opacity: 1,
          textDecoration: {
            underline: false,
            overline: false,
            lineThrough: false,
            color: '#000000FF',
            style: 'solid',
            thickness: 1
          },
          letterSpacing: 0,
          wordSpacing: 0,
          transform: {
            scaleX: 1,
            scaleY: 1,
            skewX: 0,
            skewY: 0,
            rotate: 0
          },
          direction: 'ltr'
        });

        x += charWidth;
      }

      return layouts;
    };
  }
}

// 使用示例
const gracefulDegradation = new GracefulDegradation(parser);

const result = await gracefulDegradation.parseWithGracefulDegradation(html, {
  viewportWidth: 800
});

if (result.degraded) {
  console.warn(`使用降级模式: ${result.reason}`);
  // 可能需要通知用户或记录日志
}

// 使用结果
renderToCanvas(ctx, result.layouts);

错误边界组件

typescript
class ParserErrorBoundary {
  private parser: HtmlLayoutParser;
  private errorCallback?: (error: Error, html: string) => void;
  private maxConsecutiveErrors: number;
  private consecutiveErrors: number = 0;
  private lastErrorTime: number = 0;

  constructor(
    parser: HtmlLayoutParser,
    options: {
      maxConsecutiveErrors?: number;
      errorCallback?: (error: Error, html: string) => void;
    } = {}
  ) {
    this.parser = parser;
    this.maxConsecutiveErrors = options.maxConsecutiveErrors || 5;
    this.errorCallback = options.errorCallback;
  }

  async safeParse(
    html: string,
    options: { viewportWidth: number }
  ): Promise<CharLayout[] | null> {
    // 检查是否应该暂停解析(连续错误过多)
    if (this.shouldPause()) {
      console.warn('连续错误过多,暂停解析');
      return null;
    }

    try {
      const layouts = this.parser.parse(html, options);
      
      // 成功时重置错误计数
      this.consecutiveErrors = 0;
      return layouts;
      
    } catch (error) {
      this.handleError(error, html);
      return null;
    }
  }

  private shouldPause(): boolean {
    const now = Date.now();
    const timeSinceLastError = now - this.lastErrorTime;
    
    // 如果距离上次错误超过 1 分钟,重置计数
    if (timeSinceLastError > 60000) {
      this.consecutiveErrors = 0;
      return false;
    }

    return this.consecutiveErrors >= this.maxConsecutiveErrors;
  }

  private handleError(error: Error, html: string): void {
    this.consecutiveErrors++;
    this.lastErrorTime = Date.now();

    console.error(`解析错误 (连续第 ${this.consecutiveErrors} 次):`, error.message);

    if (this.errorCallback) {
      try {
        this.errorCallback(error, html);
      } catch (callbackError) {
        console.error('错误回调函数执行失败:', callbackError);
      }
    }

    // 如果连续错误过多,建议重启解析器
    if (this.consecutiveErrors >= this.maxConsecutiveErrors) {
      console.error('连续错误过多,建议重启解析器');
    }
  }

  reset(): void {
    this.consecutiveErrors = 0;
    this.lastErrorTime = 0;
  }

  getErrorCount(): number {
    return this.consecutiveErrors;
  }
}

// 使用示例
const errorBoundary = new ParserErrorBoundary(parser, {
  maxConsecutiveErrors: 3,
  errorCallback: (error, html) => {
    // 记录错误到监控系统
    console.error('解析错误:', {
      message: error.message,
      htmlLength: html.length,
      timestamp: new Date().toISOString()
    });
  }
});

// 在应用中使用
const layouts = await errorBoundary.safeParse(html, { viewportWidth: 800 });

if (layouts) {
  // 正常处理
  renderToCanvas(ctx, layouts);
} else {
  // 显示错误状态或使用备用方案
  showErrorMessage('解析失败,请稍后重试');
}

Released under the MIT License.