138 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			138 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import "package:flutter/material.dart";
 | 
						|
import "package:material_symbols_icons/material_symbols_icons.dart";
 | 
						|
import "package:styled_widget/styled_widget.dart";
 | 
						|
import "package:markdown/markdown.dart" as markdown;
 | 
						|
import "package:markdown_widget/markdown_widget.dart";
 | 
						|
 | 
						|
class ProposalBlockSyntax extends markdown.BlockSyntax {
 | 
						|
  @override
 | 
						|
  RegExp get pattern => RegExp(r'^<proposal', caseSensitive: false);
 | 
						|
 | 
						|
  @override
 | 
						|
  bool canParse(markdown.BlockParser parser) {
 | 
						|
    return pattern.hasMatch(parser.current.content);
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  bool canEndBlock(markdown.BlockParser parser) {
 | 
						|
    return parser.current.content.contains('</proposal>');
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  markdown.Node parse(markdown.BlockParser parser) {
 | 
						|
    final childLines = <String>[];
 | 
						|
 | 
						|
    // Extract type from opening tag
 | 
						|
    final openingLine = parser.current.content;
 | 
						|
    final attrsMatch = RegExp(
 | 
						|
      r'<proposal(\s[^>]*)?>',
 | 
						|
      caseSensitive: false,
 | 
						|
    ).firstMatch(openingLine);
 | 
						|
    final attrs = attrsMatch?.group(1) ?? '';
 | 
						|
    final typeMatch = RegExp(r'type="([^"]*)"').firstMatch(attrs);
 | 
						|
    final type = typeMatch?.group(1) ?? '';
 | 
						|
 | 
						|
    // Collect all lines until closing tag
 | 
						|
    while (!parser.isDone) {
 | 
						|
      childLines.add(parser.current.content);
 | 
						|
      if (canEndBlock(parser)) {
 | 
						|
        parser.advance();
 | 
						|
        break;
 | 
						|
      }
 | 
						|
      parser.advance();
 | 
						|
    }
 | 
						|
 | 
						|
    // Extract content between tags
 | 
						|
    final fullContent = childLines.join('\n');
 | 
						|
    final contentMatch = RegExp(
 | 
						|
      r'<proposal[^>]*>(.*?)</proposal>',
 | 
						|
      dotAll: true,
 | 
						|
      caseSensitive: false,
 | 
						|
    ).firstMatch(fullContent);
 | 
						|
    final content = contentMatch?.group(1)?.trim() ?? '';
 | 
						|
 | 
						|
    final element = markdown.Element('proposal', [markdown.Text(content)])
 | 
						|
      ..attributes['type'] = type;
 | 
						|
 | 
						|
    return element;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ProposalGenerator extends SpanNodeGeneratorWithTag {
 | 
						|
  ProposalGenerator({
 | 
						|
    required Color backgroundColor,
 | 
						|
    required Color foregroundColor,
 | 
						|
    required Color borderColor,
 | 
						|
  }) : super(
 | 
						|
         tag: 'proposal',
 | 
						|
         generator: (
 | 
						|
           markdown.Element element,
 | 
						|
           MarkdownConfig config,
 | 
						|
           WidgetVisitor visitor,
 | 
						|
         ) {
 | 
						|
           return ProposalSpanNode(
 | 
						|
             text: element.textContent,
 | 
						|
             type: element.attributes['type'] ?? '',
 | 
						|
             backgroundColor: backgroundColor,
 | 
						|
             foregroundColor: foregroundColor,
 | 
						|
             borderColor: borderColor,
 | 
						|
           );
 | 
						|
         },
 | 
						|
       );
 | 
						|
}
 | 
						|
 | 
						|
class ProposalSpanNode extends SpanNode {
 | 
						|
  final String text;
 | 
						|
  final String type;
 | 
						|
  final Color backgroundColor;
 | 
						|
  final Color foregroundColor;
 | 
						|
  final Color borderColor;
 | 
						|
 | 
						|
  ProposalSpanNode({
 | 
						|
    required this.text,
 | 
						|
    required this.type,
 | 
						|
    required this.backgroundColor,
 | 
						|
    required this.foregroundColor,
 | 
						|
    required this.borderColor,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  InlineSpan build() {
 | 
						|
    return WidgetSpan(
 | 
						|
      child: Container(
 | 
						|
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
 | 
						|
        decoration: BoxDecoration(
 | 
						|
          color: backgroundColor,
 | 
						|
          border: Border.all(color: borderColor, width: 1),
 | 
						|
          borderRadius: BorderRadius.circular(8),
 | 
						|
        ),
 | 
						|
        child: Column(
 | 
						|
          mainAxisSize: MainAxisSize.min,
 | 
						|
          spacing: 6,
 | 
						|
          children: [
 | 
						|
            Row(
 | 
						|
              spacing: 6,
 | 
						|
              children: [
 | 
						|
                Icon(Symbols.lightbulb, size: 16, color: foregroundColor),
 | 
						|
                Text(
 | 
						|
                  'SN-chan suggest you to ${type.split('_').reversed.join(' ')}',
 | 
						|
                ).fontSize(13).opacity(0.8),
 | 
						|
              ],
 | 
						|
            ).padding(top: 3, bottom: 4),
 | 
						|
            Flexible(
 | 
						|
              child: Text(
 | 
						|
                text,
 | 
						|
                style: TextStyle(
 | 
						|
                  color: foregroundColor,
 | 
						|
                  fontSize: 14,
 | 
						|
                  fontWeight: FontWeight.w500,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |