Commit 98762be1 authored by Steven's avatar Steven

feat: implement indent for list nodes

parent d44e74bd
...@@ -54,13 +54,13 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node { ...@@ -54,13 +54,13 @@ func convertFromASTNode(rawNode ast.Node) *apiv2pb.Node {
node.Node = &apiv2pb.Node_BlockquoteNode{BlockquoteNode: &apiv2pb.BlockquoteNode{Children: children}} node.Node = &apiv2pb.Node_BlockquoteNode{BlockquoteNode: &apiv2pb.BlockquoteNode{Children: children}}
case *ast.OrderedList: case *ast.OrderedList:
children := convertFromASTNodes(n.Children) children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_OrderedListNode{OrderedListNode: &apiv2pb.OrderedListNode{Number: n.Number, Children: children}} node.Node = &apiv2pb.Node_OrderedListNode{OrderedListNode: &apiv2pb.OrderedListNode{Number: n.Number, Indent: int32(n.Indent), Children: children}}
case *ast.UnorderedList: case *ast.UnorderedList:
children := convertFromASTNodes(n.Children) children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_UnorderedListNode{UnorderedListNode: &apiv2pb.UnorderedListNode{Symbol: n.Symbol, Children: children}} node.Node = &apiv2pb.Node_UnorderedListNode{UnorderedListNode: &apiv2pb.UnorderedListNode{Symbol: n.Symbol, Indent: int32(n.Indent), Children: children}}
case *ast.TaskList: case *ast.TaskList:
children := convertFromASTNodes(n.Children) children := convertFromASTNodes(n.Children)
node.Node = &apiv2pb.Node_TaskListNode{TaskListNode: &apiv2pb.TaskListNode{Symbol: n.Symbol, Complete: n.Complete, Children: children}} node.Node = &apiv2pb.Node_TaskListNode{TaskListNode: &apiv2pb.TaskListNode{Symbol: n.Symbol, Indent: int32(n.Indent), Complete: n.Complete, Children: children}}
case *ast.MathBlock: case *ast.MathBlock:
node.Node = &apiv2pb.Node_MathBlockNode{MathBlockNode: &apiv2pb.MathBlockNode{Content: n.Content}} node.Node = &apiv2pb.Node_MathBlockNode{MathBlockNode: &apiv2pb.MathBlockNode{Content: n.Content}}
case *ast.Text: case *ast.Text:
...@@ -123,13 +123,13 @@ func convertToASTNode(node *apiv2pb.Node) ast.Node { ...@@ -123,13 +123,13 @@ func convertToASTNode(node *apiv2pb.Node) ast.Node {
return &ast.Blockquote{Children: children} return &ast.Blockquote{Children: children}
case *apiv2pb.Node_OrderedListNode: case *apiv2pb.Node_OrderedListNode:
children := convertToASTNodes(n.OrderedListNode.Children) children := convertToASTNodes(n.OrderedListNode.Children)
return &ast.OrderedList{Number: n.OrderedListNode.Number, Children: children} return &ast.OrderedList{Number: n.OrderedListNode.Number, Indent: int(n.OrderedListNode.Indent), Children: children}
case *apiv2pb.Node_UnorderedListNode: case *apiv2pb.Node_UnorderedListNode:
children := convertToASTNodes(n.UnorderedListNode.Children) children := convertToASTNodes(n.UnorderedListNode.Children)
return &ast.UnorderedList{Symbol: n.UnorderedListNode.Symbol, Children: children} return &ast.UnorderedList{Symbol: n.UnorderedListNode.Symbol, Indent: int(n.UnorderedListNode.Indent), Children: children}
case *apiv2pb.Node_TaskListNode: case *apiv2pb.Node_TaskListNode:
children := convertToASTNodes(n.TaskListNode.Children) children := convertToASTNodes(n.TaskListNode.Children)
return &ast.TaskList{Symbol: n.TaskListNode.Symbol, Complete: n.TaskListNode.Complete, Children: children} return &ast.TaskList{Symbol: n.TaskListNode.Symbol, Indent: int(n.TaskListNode.Indent), Complete: n.TaskListNode.Complete, Children: children}
case *apiv2pb.Node_MathBlockNode: case *apiv2pb.Node_MathBlockNode:
return &ast.MathBlock{Content: n.MathBlockNode.Content} return &ast.MathBlock{Content: n.MathBlockNode.Content}
case *apiv2pb.Node_TextNode: case *apiv2pb.Node_TextNode:
......
package ast package ast
import "fmt" import (
"fmt"
"strings"
)
type BaseBlock struct { type BaseBlock struct {
BaseNode BaseNode
...@@ -110,7 +113,10 @@ func (n *Blockquote) Restore() string { ...@@ -110,7 +113,10 @@ func (n *Blockquote) Restore() string {
type OrderedList struct { type OrderedList struct {
BaseBlock BaseBlock
Number string // Number is the number of the list.
Number string
// Indent is the number of spaces.
Indent int
Children []Node Children []Node
} }
...@@ -123,14 +129,16 @@ func (n *OrderedList) Restore() string { ...@@ -123,14 +129,16 @@ func (n *OrderedList) Restore() string {
for _, child := range n.Children { for _, child := range n.Children {
result += child.Restore() result += child.Restore()
} }
return fmt.Sprintf("%s. %s", n.Number, result) return fmt.Sprintf("%s%s. %s", strings.Repeat(" ", n.Indent), n.Number, result)
} }
type UnorderedList struct { type UnorderedList struct {
BaseBlock BaseBlock
// Symbol is "*" or "-" or "+". // Symbol is "*" or "-" or "+".
Symbol string Symbol string
// Indent is the number of spaces.
Indent int
Children []Node Children []Node
} }
...@@ -143,14 +151,16 @@ func (n *UnorderedList) Restore() string { ...@@ -143,14 +151,16 @@ func (n *UnorderedList) Restore() string {
for _, child := range n.Children { for _, child := range n.Children {
result += child.Restore() result += child.Restore()
} }
return fmt.Sprintf("%s %s", n.Symbol, result) return fmt.Sprintf("%s%s %s", strings.Repeat(" ", n.Indent), n.Symbol, result)
} }
type TaskList struct { type TaskList struct {
BaseBlock BaseBlock
// Symbol is "*" or "-" or "+". // Symbol is "*" or "-" or "+".
Symbol string Symbol string
// Indent is the number of spaces.
Indent int
Complete bool Complete bool
Children []Node Children []Node
} }
...@@ -168,7 +178,7 @@ func (n *TaskList) Restore() string { ...@@ -168,7 +178,7 @@ func (n *TaskList) Restore() string {
if n.Complete { if n.Complete {
complete = "x" complete = "x"
} }
return fmt.Sprintf("%s [%s] %s", n.Symbol, complete, result) return fmt.Sprintf("%s%s [%s] %s", strings.Repeat(" ", n.Indent), n.Symbol, complete, result)
} }
type MathBlock struct { type MathBlock struct {
......
...@@ -17,12 +17,22 @@ func (*OrderedListParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -17,12 +17,22 @@ func (*OrderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 4 { if len(tokens) < 4 {
return 0, false return 0, false
} }
if tokens[0].Type != tokenizer.Number || tokens[1].Type != tokenizer.Dot || tokens[2].Type != tokenizer.Space {
indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
corsor := indent
if tokens[corsor].Type != tokenizer.Number || tokens[corsor+1].Type != tokenizer.Dot || tokens[corsor+2].Type != tokenizer.Space {
return 0, false return 0, false
} }
contentTokens := []*tokenizer.Token{} contentTokens := []*tokenizer.Token{}
for _, token := range tokens[3:] { for _, token := range tokens[corsor+3:] {
if token.Type == tokenizer.Newline { if token.Type == tokenizer.Newline {
break break
} }
...@@ -33,7 +43,7 @@ func (*OrderedListParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -33,7 +43,7 @@ func (*OrderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
return 0, false return 0, false
} }
return len(contentTokens) + 3, true return indent + len(contentTokens) + 3, true
} }
func (p *OrderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { func (p *OrderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
...@@ -42,13 +52,22 @@ func (p *OrderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { ...@@ -42,13 +52,22 @@ func (p *OrderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
return nil, errors.New("not matched") return nil, errors.New("not matched")
} }
contentTokens := tokens[3:size] indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
contentTokens := tokens[indent+3 : size]
children, err := ParseInline(contentTokens) children, err := ParseInline(contentTokens)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ast.OrderedList{ return &ast.OrderedList{
Number: tokens[0].Value, Number: tokens[indent].Value,
Indent: indent,
Children: children, Children: children,
}, nil }, nil
} }
...@@ -30,6 +30,18 @@ func TestOrderedListParser(t *testing.T) { ...@@ -30,6 +30,18 @@ func TestOrderedListParser(t *testing.T) {
}, },
}, },
}, },
{
text: " 1. Hello World",
node: &ast.OrderedList{
Number: "1",
Indent: 2,
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{ {
text: "1aa. Hello World", text: "1aa. Hello World",
node: nil, node: nil,
......
...@@ -26,9 +26,6 @@ func (*ParagraphParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -26,9 +26,6 @@ func (*ParagraphParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(contentTokens) == 0 { if len(contentTokens) == 0 {
return 0, false return 0, false
} }
if len(contentTokens) == 1 && contentTokens[0].Type == tokenizer.Newline {
return 0, false
}
return len(contentTokens), true return len(contentTokens), true
} }
......
...@@ -18,22 +18,30 @@ func (*TaskListParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -18,22 +18,30 @@ func (*TaskListParser) Match(tokens []*tokenizer.Token) (int, bool) {
return 0, false return 0, false
} }
symbolToken := tokens[0] indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
symbolToken := tokens[indent]
if symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign { if symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign {
return 0, false return 0, false
} }
if tokens[1].Type != tokenizer.Space { if tokens[indent+1].Type != tokenizer.Space {
return 0, false return 0, false
} }
if tokens[2].Type != tokenizer.LeftSquareBracket || (tokens[3].Type != tokenizer.Space && tokens[3].Value != "x") || tokens[4].Type != tokenizer.RightSquareBracket { if tokens[indent+2].Type != tokenizer.LeftSquareBracket || (tokens[indent+3].Type != tokenizer.Space && tokens[indent+3].Value != "x") || tokens[indent+4].Type != tokenizer.RightSquareBracket {
return 0, false return 0, false
} }
if tokens[5].Type != tokenizer.Space { if tokens[indent+5].Type != tokenizer.Space {
return 0, false return 0, false
} }
contentTokens := []*tokenizer.Token{} contentTokens := []*tokenizer.Token{}
for _, token := range tokens[6:] { for _, token := range tokens[indent+6:] {
if token.Type == tokenizer.Newline { if token.Type == tokenizer.Newline {
break break
} }
...@@ -43,7 +51,7 @@ func (*TaskListParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -43,7 +51,7 @@ func (*TaskListParser) Match(tokens []*tokenizer.Token) (int, bool) {
return 0, false return 0, false
} }
return len(contentTokens) + 6, true return indent + len(contentTokens) + 6, true
} }
func (p *TaskListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { func (p *TaskListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
...@@ -52,15 +60,24 @@ func (p *TaskListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { ...@@ -52,15 +60,24 @@ func (p *TaskListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
return nil, errors.New("not matched") return nil, errors.New("not matched")
} }
symbolToken := tokens[0] indent := 0
contentTokens := tokens[6:size] for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
symbolToken := tokens[indent]
contentTokens := tokens[indent+6 : size]
children, err := ParseInline(contentTokens) children, err := ParseInline(contentTokens)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ast.TaskList{ return &ast.TaskList{
Symbol: symbolToken.Type, Symbol: symbolToken.Type,
Complete: tokens[3].Value == "x", Indent: indent,
Complete: tokens[indent+3].Value == "x",
Children: children, Children: children,
}, nil }, nil
} }
...@@ -31,6 +31,19 @@ func TestTaskListParser(t *testing.T) { ...@@ -31,6 +31,19 @@ func TestTaskListParser(t *testing.T) {
}, },
}, },
}, },
{
text: " + [ ] Hello World",
node: &ast.TaskList{
Symbol: tokenizer.PlusSign,
Indent: 2,
Complete: false,
Children: []ast.Node{
&ast.Text{
Content: "Hello World",
},
},
},
},
{ {
text: "* [x] **Hello**", text: "* [x] **Hello**",
node: &ast.TaskList{ node: &ast.TaskList{
......
...@@ -17,13 +17,23 @@ func (*UnorderedListParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -17,13 +17,23 @@ func (*UnorderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
if len(tokens) < 3 { if len(tokens) < 3 {
return 0, false return 0, false
} }
symbolToken := tokens[0]
if (symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign) || tokens[1].Type != tokenizer.Space { indent := 0
for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
corsor := indent
symbolToken := tokens[corsor]
if (symbolToken.Type != tokenizer.Hyphen && symbolToken.Type != tokenizer.Asterisk && symbolToken.Type != tokenizer.PlusSign) || tokens[corsor+1].Type != tokenizer.Space {
return 0, false return 0, false
} }
contentTokens := []*tokenizer.Token{} contentTokens := []*tokenizer.Token{}
for _, token := range tokens[2:] { for _, token := range tokens[corsor+2:] {
if token.Type == tokenizer.Newline { if token.Type == tokenizer.Newline {
break break
} }
...@@ -33,7 +43,7 @@ func (*UnorderedListParser) Match(tokens []*tokenizer.Token) (int, bool) { ...@@ -33,7 +43,7 @@ func (*UnorderedListParser) Match(tokens []*tokenizer.Token) (int, bool) {
return 0, false return 0, false
} }
return len(contentTokens) + 2, true return indent + len(contentTokens) + 2, true
} }
func (p *UnorderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) { func (p *UnorderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) {
...@@ -42,14 +52,23 @@ func (p *UnorderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error) ...@@ -42,14 +52,23 @@ func (p *UnorderedListParser) Parse(tokens []*tokenizer.Token) (ast.Node, error)
return nil, errors.New("not matched") return nil, errors.New("not matched")
} }
symbolToken := tokens[0] indent := 0
contentTokens := tokens[2:size] for _, token := range tokens {
if token.Type == tokenizer.Space {
indent++
} else {
break
}
}
symbolToken := tokens[indent]
contentTokens := tokens[indent+2 : size]
children, err := ParseInline(contentTokens) children, err := ParseInline(contentTokens)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ast.UnorderedList{ return &ast.UnorderedList{
Symbol: symbolToken.Type, Symbol: symbolToken.Type,
Indent: indent,
Children: children, Children: children,
}, nil }, nil
} }
...@@ -103,18 +103,21 @@ message BlockquoteNode { ...@@ -103,18 +103,21 @@ message BlockquoteNode {
message OrderedListNode { message OrderedListNode {
string number = 1; string number = 1;
repeated Node children = 2; int32 indent = 2;
repeated Node children = 3;
} }
message UnorderedListNode { message UnorderedListNode {
string symbol = 1; string symbol = 1;
repeated Node children = 2; int32 indent = 2;
repeated Node children = 3;
} }
message TaskListNode { message TaskListNode {
string symbol = 1; string symbol = 1;
bool complete = 2; int32 indent = 2;
repeated Node children = 3; bool complete = 3;
repeated Node children = 4;
} }
message MathBlockNode { message MathBlockNode {
......
...@@ -1233,6 +1233,7 @@ ...@@ -1233,6 +1233,7 @@
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| number | [string](#string) | | | | number | [string](#string) | | |
| indent | [int32](#int32) | | |
| children | [Node](#memos-api-v2-Node) | repeated | | | children | [Node](#memos-api-v2-Node) | repeated | |
...@@ -1324,6 +1325,7 @@ ...@@ -1324,6 +1325,7 @@
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| symbol | [string](#string) | | | | symbol | [string](#string) | | |
| indent | [int32](#int32) | | |
| complete | [bool](#bool) | | | | complete | [bool](#bool) | | |
| children | [Node](#memos-api-v2-Node) | repeated | | | children | [Node](#memos-api-v2-Node) | repeated | |
...@@ -1356,6 +1358,7 @@ ...@@ -1356,6 +1358,7 @@
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| symbol | [string](#string) | | | | symbol | [string](#string) | | |
| indent | [int32](#int32) | | |
| children | [Node](#memos-api-v2-Node) | repeated | | | children | [Node](#memos-api-v2-Node) | repeated | |
......
This diff is collapsed.
import { repeat } from "lodash-es";
import { Node } from "@/types/proto/api/v2/markdown_service"; import { Node } from "@/types/proto/api/v2/markdown_service";
import Renderer from "./Renderer"; import Renderer from "./Renderer";
import { BaseProps } from "./types"; import { BaseProps } from "./types";
interface Props extends BaseProps { interface Props extends BaseProps {
number: string; number: string;
indent: number;
children: Node[]; children: Node[];
} }
const OrderedList: React.FC<Props> = ({ number, children }: Props) => { const OrderedList: React.FC<Props> = ({ number, indent, children }: Props) => {
return ( return (
<ol> <ol>
<li className="grid grid-cols-[24px_1fr] gap-1"> <li className="w-full flex flex-row">
<div className="w-7 h-6 flex justify-center items-center"> <div className="block font-mono shrink-0">
<span className="opacity-80">{number}.</span> <span>{repeat(" ", indent)}</span>
</div> </div>
<div> <div className="w-auto grid grid-cols-[24px_1fr] gap-1">
{children.map((child, index) => ( <div className="w-7 h-6 flex justify-center items-center">
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} /> <span className="opacity-80">{number}.</span>
))} </div>
<div>
{children.map((child, index) => (
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} />
))}
</div>
</div> </div>
</li> </li>
</ol> </ol>
......
import { Checkbox } from "@mui/joy"; import { Checkbox } from "@mui/joy";
import classNames from "classnames";
import { repeat } from "lodash-es";
import { useContext } from "react"; import { useContext } from "react";
import { useMemoStore } from "@/store/v1"; import { useMemoStore } from "@/store/v1";
import { Node, NodeType } from "@/types/proto/api/v2/markdown_service"; import { Node, NodeType } from "@/types/proto/api/v2/markdown_service";
...@@ -8,11 +10,12 @@ import { RendererContext } from "./types"; ...@@ -8,11 +10,12 @@ import { RendererContext } from "./types";
interface Props { interface Props {
index: string; index: string;
symbol: string; symbol: string;
indent: number;
complete: boolean; complete: boolean;
children: Node[]; children: Node[];
} }
const TaskList: React.FC<Props> = ({ index, complete, children }: Props) => { const TaskList: React.FC<Props> = ({ index, indent, complete, children }: Props) => {
const context = useContext(RendererContext); const context = useContext(RendererContext);
const memoStore = useMemoStore(); const memoStore = useMemoStore();
...@@ -43,14 +46,19 @@ const TaskList: React.FC<Props> = ({ index, complete, children }: Props) => { ...@@ -43,14 +46,19 @@ const TaskList: React.FC<Props> = ({ index, complete, children }: Props) => {
return ( return (
<ul> <ul>
<li className="grid grid-cols-[24px_1fr] gap-1"> <li className="w-full flex flex-row">
<div className="w-7 h-6 flex justify-center items-center"> <div className="block font-mono shrink-0">
<Checkbox size="sm" checked={complete} disabled={context.readonly} onChange={(e) => handleCheckboxChange(e.target.checked)} /> <span>{repeat(" ", indent)}</span>
</div> </div>
<div> <div className="w-auto grid grid-cols-[24px_1fr] gap-1">
{children.map((child, subIndex) => ( <div className="w-7 h-6 flex justify-center items-center">
<Renderer key={`${child.type}-${subIndex}`} index={`${index}-${subIndex}`} node={child} /> <Checkbox size="sm" checked={complete} disabled={context.readonly} onChange={(e) => handleCheckboxChange(e.target.checked)} />
))} </div>
<div className={classNames(complete && "line-through opacity-80")}>
{children.map((child, index) => (
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} />
))}
</div>
</div> </div>
</li> </li>
</ul> </ul>
......
import { repeat } from "lodash-es";
import { Node } from "@/types/proto/api/v2/markdown_service"; import { Node } from "@/types/proto/api/v2/markdown_service";
import Renderer from "./Renderer"; import Renderer from "./Renderer";
interface Props { interface Props {
symbol: string; symbol: string;
indent: number;
children: Node[]; children: Node[];
} }
const UnorderedList: React.FC<Props> = ({ children }: Props) => { const UnorderedList: React.FC<Props> = ({ indent, children }: Props) => {
return ( return (
<ul> <ul>
<li className="grid grid-cols-[24px_1fr] gap-1"> <li className="w-full flex flex-row">
<div className="w-7 h-6 flex justify-center items-center"> <div className="block font-mono shrink-0">
<span className="opacity-80"></span> <span>{repeat(" ", indent)}</span>
</div> </div>
<div> <div className="w-auto grid grid-cols-[24px_1fr] gap-1">
{children.map((child, index) => ( <div className="w-7 h-6 flex justify-center items-center">
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} /> <span className="opacity-80"></span>
))} </div>
<div>
{children.map((child, index) => (
<Renderer key={`${child.type}-${index}`} index={String(index)} node={child} />
))}
</div>
</div> </div>
</li> </li>
</ul> </ul>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment