Merge pull request #1184 from LucasXu0/latex

feat: implement TeX plugin
This commit is contained in:
Lucas.Xu 2022-10-06 10:53:35 +08:00 committed by GitHub
commit 8a2c57014c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 201 additions and 2 deletions

View File

@ -9,6 +9,12 @@
"align": "center"
}
},
{
"type": "tex",
"attributes": {
"tex": "x = 2"
}
},
{
"type": "text",
"attributes": { "subtype": "heading", "heading": "h1" },

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:example/plugin/code_block_node_widget.dart';
import 'package:example/plugin/tex_block_node_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -119,6 +120,7 @@ class _MyHomePageState extends State<MyHomePage> {
editable: true,
customBuilders: {
'text/code_block': CodeBlockNodeWidgetBuilder(),
'tex': TeXBlockNodeWidgetBuidler(),
},
shortcutEvents: [
enterInCodeBlock,
@ -126,7 +128,8 @@ class _MyHomePageState extends State<MyHomePage> {
underscoreToItalic,
],
selectionMenuItems: [
codeBlockItem,
codeBlockMenuItem,
teXBlockMenuItem,
],
),
);

View File

@ -44,7 +44,7 @@ ShortcutEventHandler _ignorekHandler = (editorState, event) {
return KeyEventResult.ignored;
};
SelectionMenuItem codeBlockItem = SelectionMenuItem(
SelectionMenuItem codeBlockMenuItem = SelectionMenuItem(
name: () => 'Code Block',
icon: const Icon(Icons.abc),
keywords: ['code block'],

View File

@ -0,0 +1,189 @@
import 'dart:collection';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter_math_fork/flutter_math.dart';
SelectionMenuItem teXBlockMenuItem = SelectionMenuItem(
name: 'Tex',
icon: const Icon(Icons.text_fields_rounded),
keywords: ['tex, latex, katex'],
handler: (editorState, _, __) {
final selection =
editorState.service.selectionService.currentSelection.value;
final textNodes = editorState.service.selectionService.currentSelectedNodes
.whereType<TextNode>();
if (selection == null || !selection.isCollapsed || textNodes.isEmpty) {
return;
}
final Path texNodePath;
if (textNodes.first.toRawString().isEmpty) {
texNodePath = selection.end.path;
TransactionBuilder(editorState)
..insertNode(
selection.end.path,
Node(
type: 'tex',
children: LinkedList(),
attributes: {'tex': ''},
),
)
..deleteNode(textNodes.first)
..afterSelection = selection
..commit();
} else {
texNodePath = selection.end.path.next;
TransactionBuilder(editorState)
..insertNode(
selection.end.path.next,
Node(
type: 'tex',
children: LinkedList(),
attributes: {'tex': ''},
),
)
..afterSelection = selection
..commit();
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final texState =
editorState.document.nodeAtPath(texNodePath)?.key?.currentState;
if (texState != null && texState is __TeXBlockNodeWidgetState) {
texState.showEditingDialog();
}
});
},
);
class TeXBlockNodeWidgetBuidler extends NodeWidgetBuilder<Node> {
@override
Widget build(NodeWidgetContext<Node> context) {
return _TeXBlockNodeWidget(
key: context.node.key,
node: context.node,
editorState: context.editorState,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return node.attributes['tex'] is String;
};
}
class _TeXBlockNodeWidget extends StatefulWidget {
const _TeXBlockNodeWidget({
Key? key,
required this.node,
required this.editorState,
}) : super(key: key);
final Node node;
final EditorState editorState;
@override
State<_TeXBlockNodeWidget> createState() => __TeXBlockNodeWidgetState();
}
class __TeXBlockNodeWidgetState extends State<_TeXBlockNodeWidget> {
String get _tex => widget.node.attributes['tex'] as String;
bool _isHover = false;
@override
Widget build(BuildContext context) {
return InkWell(
onHover: (value) {
setState(() {
_isHover = value;
});
},
onTap: () {
showEditingDialog();
},
child: Stack(
children: [
_buildTex(context),
if (_isHover) _buildDeleteButton(context),
],
),
);
}
Widget _buildTex(BuildContext context) {
return Container(
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: _isHover ? Colors.grey[200] : Colors.transparent,
),
child: Center(
child: Math.tex(
_tex,
textStyle: const TextStyle(fontSize: 20),
mathStyle: MathStyle.display,
),
),
);
}
Widget _buildDeleteButton(BuildContext context) {
return Positioned(
top: -5,
right: -5,
child: IconButton(
icon: Icon(
Icons.delete_outline,
color: Colors.blue[400],
size: 16,
),
onPressed: () {
TransactionBuilder(widget.editorState)
..deleteNode(widget.node)
..commit();
},
),
);
}
void showEditingDialog() {
showDialog(
context: context,
builder: (context) {
final controller = TextEditingController(text: _tex);
return AlertDialog(
title: const Text('Edit Katex'),
content: TextField(
controller: controller,
maxLines: null,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
if (controller.text != _tex) {
TransactionBuilder(widget.editorState)
..updateNode(
widget.node,
{'tex': controller.text},
)
..commit();
}
},
child: const Text('OK'),
),
],
);
},
);
}
}

View File

@ -44,6 +44,7 @@ dependencies:
file_picker: ^5.0.1
universal_html: ^2.0.8
highlight: ^0.7.0
flutter_math_fork: ^0.6.3+1
dev_dependencies:
flutter_test: