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,
 | |
|                 ),
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 |