Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F96564838
PhutilRemarkupEngineRemarkupListBlockRule.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Dec 28, 04:47
Size
13 KB
Mime Type
text/x-php
Expires
Mon, Dec 30, 04:47 (2 d)
Engine
blob
Format
Raw Data
Handle
23207235
Attached To
rPHU libphutil
PhutilRemarkupEngineRemarkupListBlockRule.php
View Options
<?php
final
class
PhutilRemarkupEngineRemarkupListBlockRule
extends
PhutilRemarkupEngineBlockRule
{
/**
* This rule must apply before the Code block rule because it needs to
* win blocks which begin ` - Lorem ipsum`.
*/
public
function
getPriority
()
{
return
400
;
}
public
function
getMatchingLineCount
(
array
$lines
,
$cursor
)
{
$num_lines
=
0
;
$first_line
=
$cursor
;
$is_one_line
=
false
;
while
(
isset
(
$lines
[
$cursor
]))
{
if
(!
$num_lines
)
{
if
(
preg_match
(
self
::
START_BLOCK_PATTERN
,
$lines
[
$cursor
]))
{
$num_lines
++;
$cursor
++;
$is_one_line
=
true
;
continue
;
}
}
else
{
if
(
preg_match
(
self
::
CONT_BLOCK_PATTERN
,
$lines
[
$cursor
]))
{
$num_lines
++;
$cursor
++;
$is_one_line
=
false
;
continue
;
}
if
(
strlen
(
trim
(
$lines
[
$cursor
]))
&&
strlen
(
$lines
[
$cursor
])
!==
strlen
(
ltrim
(
$lines
[
$cursor
])))
{
$num_lines
++;
$cursor
++;
continue
;
}
if
(!
strlen
(
trim
(
$lines
[
$cursor
])))
{
$num_lines
++;
}
}
break
;
}
// If this list only has one item in it, and the list marker is "#", and
// it's not the last line in the input, parse it as a header instead of a
// list. This produces better behavior for alternate Markdown headers.
if
(
$is_one_line
)
{
if
((
$first_line
+
$num_lines
)
<
count
(
$lines
))
{
if
(
strncmp
(
$lines
[
$first_line
],
'#'
,
1
)
===
0
)
{
return
0
;
}
}
}
return
$num_lines
;
}
/**
* The maximum sub-list depth you can nest to. Avoids silliness and blowing
* the stack.
*/
const
MAXIMUM_LIST_NESTING_DEPTH
=
12
;
const
START_BLOCK_PATTERN
=
'@^
\s
*(?:[-*#]+|1[.)]|
\[
.?
\]
)
\s
+@'
;
const
CONT_BLOCK_PATTERN
=
'@^
\s
*(?:[-*#]+|[0-9]+[.)]|
\[
.?
\]
)
\s
+@'
;
const
STRIP_BLOCK_PATTERN
=
'@^
\s
*(?:[-*#]+|[0-9]+[.)])
\s
*@'
;
public
function
markupText
(
$text
,
$children
)
{
$items
=
array
();
$lines
=
explode
(
"
\n
"
,
$text
);
// We allow users to delimit lists using either differing indentation
// levels:
//
// - a
// - b
//
// ...or differing numbers of item-delimiter characters:
//
// - a
// -- b
//
// If they use the second style but block-indent the whole list, we'll
// get the depth counts wrong for the first item. To prevent this,
// un-indent every item by the minimum indentation level for the whole
// block before we begin parsing.
$regex
=
self
::
START_BLOCK_PATTERN
;
$min_space
=
PHP_INT_MAX
;
foreach
(
$lines
as
$ii
=>
$line
)
{
$matches
=
null
;
if
(
preg_match
(
$regex
,
$line
))
{
$regex
=
self
::
CONT_BLOCK_PATTERN
;
if
(
preg_match
(
'/^(
\s
+)/'
,
$line
,
$matches
))
{
$space
=
strlen
(
$matches
[
1
]);
}
else
{
$space
=
0
;
}
$min_space
=
min
(
$min_space
,
$space
);
}
}
$regex
=
self
::
START_BLOCK_PATTERN
;
if
(
$min_space
)
{
foreach
(
$lines
as
$key
=>
$line
)
{
if
(
preg_match
(
$regex
,
$line
))
{
$regex
=
self
::
CONT_BLOCK_PATTERN
;
$lines
[
$key
]
=
substr
(
$line
,
$min_space
);
}
}
}
// The input text may have linewraps in it, like this:
//
// - derp derp derp derp
// derp derp derp derp
// - blarp blarp blarp blarp
//
// Group text lines together into list items, stored in $items. So the
// result in the above case will be:
//
// array(
// array(
// "- derp derp derp derp",
// " derp derp derp derp",
// ),
// array(
// "- blarp blarp blarp blarp",
// ),
// );
$item
=
array
();
$regex
=
self
::
START_BLOCK_PATTERN
;
foreach
(
$lines
as
$line
)
{
if
(
preg_match
(
$regex
,
$line
))
{
$regex
=
self
::
CONT_BLOCK_PATTERN
;
if
(
$item
)
{
$items
[]
=
$item
;
$item
=
array
();
}
}
$item
[]
=
$line
;
}
if
(
$item
)
{
$items
[]
=
$item
;
}
// Process each item to normalize the text, remove line wrapping, and
// determine its depth (indentation level) and style (ordered vs unordered).
//
// Given the above example, the processed array will look like:
//
// array(
// array(
// 'text' => 'derp derp derp derp derp derp derp derp',
// 'depth' => 0,
// 'style' => '-',
// ),
// array(
// 'text' => 'blarp blarp blarp blarp',
// 'depth' => 0,
// 'style' => '-',
// ),
// );
$has_marks
=
false
;
foreach
(
$items
as
$key
=>
$item
)
{
$item
=
preg_replace
(
'/
\s
*
\n\s
*/'
,
' '
,
implode
(
"
\n
"
,
$item
));
$item
=
rtrim
(
$item
);
if
(!
strlen
(
$item
))
{
unset
(
$items
[
$key
]);
continue
;
}
$matches
=
null
;
if
(
preg_match
(
'/^
\s
*([-*#]{2,})/'
,
$item
,
$matches
))
{
// Alternate-style indents; use number of list item symbols.
$depth
=
strlen
(
$matches
[
1
])
-
1
;
}
else
if
(
preg_match
(
'/^(
\s
+)/'
,
$item
,
$matches
))
{
// Markdown-style indents; use indent depth.
$depth
=
strlen
(
$matches
[
1
]);
}
else
{
$depth
=
0
;
}
if
(
preg_match
(
'/^
\s
*(?:#|[0-9])/'
,
$item
))
{
$style
=
'#'
;
}
else
{
$style
=
'-'
;
}
// Strip leading indicators off the item.
$text
=
preg_replace
(
self
::
STRIP_BLOCK_PATTERN
,
''
,
$item
);
// Look for "[]", "[ ]", "[*]", "[x]", etc., which we render as a
// checkbox.
$mark
=
null
;
$matches
=
null
;
if
(
preg_match
(
'/^
\s
*
\[
(.?)
\]\s
*/'
,
$text
,
$matches
))
{
if
(
strlen
(
trim
(
$matches
[
1
])))
{
$mark
=
true
;
}
else
{
$mark
=
false
;
}
$has_marks
=
true
;
$text
=
substr
(
$text
,
strlen
(
$matches
[
0
]));
}
$items
[
$key
]
=
array
(
'text'
=>
$text
,
'depth'
=>
$depth
,
'style'
=>
$style
,
'mark'
=>
$mark
,
);
}
$items
=
array_values
(
$items
);
// Users can create a sub-list by indenting any deeper amount than the
// previous list, so these are both valid:
//
// - a
// - b
//
// - a
// - b
//
// In the former case, we'll have depths (0, 2). In the latter case, depths
// (0, 4). We don't actually care about how many spaces there are, only
// how many list indentation levels (that is, we want to map both of
// those cases to (0, 1), indicating "outermost list" and "first sublist").
//
// This is made more complicated because lists at two different indentation
// levels might be at the same list level:
//
// - a
// - b
// - c
// - d
//
// Here, 'b' and 'd' are at the same list level (2) but different indent
// levels (2, 4).
//
// Users can also create "staircases" like this:
//
// - a
// - b
// # c
//
// While this is silly, we'd like to render it as faithfully as possible.
//
// In order to do this, we convert the list of nodes into a tree,
// normalizing indentation levels and inserting dummy nodes as necessary to
// make the tree well-formed. See additional notes at buildTree().
//
// In the case above, the result is a tree like this:
//
// - <null>
// - <null>
// - a
// - b
// # c
$l
=
0
;
$r
=
count
(
$items
);
$tree
=
$this
->
buildTree
(
$items
,
$l
,
$r
,
$cur_level
=
0
);
// We may need to open a list on a <null> node, but they do not have
// list style information yet. We need to propagate list style information
// backward through the tree. In the above example, the tree now looks
// like this:
//
// - <null (style=#)>
// - <null (style=-)>
// - a
// - b
// # c
$this
->
adjustTreeStyleInformation
(
$tree
);
// Finally, we have enough information to render the tree.
$out
=
$this
->
renderTree
(
$tree
,
0
,
$has_marks
);
if
(
$this
->
getEngine
()->
isTextMode
())
{
$out
=
implode
(
''
,
$out
);
$out
=
rtrim
(
$out
,
"
\n
"
);
$out
=
preg_replace
(
'/ +$/m'
,
''
,
$out
);
return
$out
;
}
return
phutil_implode_html
(
''
,
$out
);
}
/**
* See additional notes in @{method:markupText}.
*/
private
function
buildTree
(
array
$items
,
$l
,
$r
,
$cur_level
)
{
if
(
$l
==
$r
)
{
return
array
();
}
if
(
$cur_level
>
self
::
MAXIMUM_LIST_NESTING_DEPTH
)
{
// This algorithm is recursive and we don't need you blowing the stack
// with your oh-so-clever 50,000-item-deep list. Cap indentation levels
// at a reasonable number and just shove everything deeper up to this
// level.
$nodes
=
array
();
for
(
$ii
=
$l
;
$ii
<
$r
;
$ii
++)
{
$nodes
[]
=
array
(
'level'
=>
$cur_level
,
'items'
=>
array
(),
)
+
$items
[
$ii
];
}
return
$nodes
;
}
$min
=
$l
;
for
(
$ii
=
$r
-
1
;
$ii
>=
$l
;
$ii
--)
{
if
(
$items
[
$ii
][
'depth'
]
<=
$items
[
$min
][
'depth'
])
{
$min
=
$ii
;
}
}
$min_depth
=
$items
[
$min
][
'depth'
];
$nodes
=
array
();
if
(
$min
!=
$l
)
{
$nodes
[]
=
array
(
'text'
=>
null
,
'level'
=>
$cur_level
,
'style'
=>
null
,
'mark'
=>
null
,
'items'
=>
$this
->
buildTree
(
$items
,
$l
,
$min
,
$cur_level
+
1
),
);
}
$last
=
$min
;
for
(
$ii
=
$last
+
1
;
$ii
<
$r
;
$ii
++)
{
if
(
$items
[
$ii
][
'depth'
]
==
$min_depth
)
{
$nodes
[]
=
array
(
'level'
=>
$cur_level
,
'items'
=>
$this
->
buildTree
(
$items
,
$last
+
1
,
$ii
,
$cur_level
+
1
),
)
+
$items
[
$last
];
$last
=
$ii
;
}
}
$nodes
[]
=
array
(
'level'
=>
$cur_level
,
'items'
=>
$this
->
buildTree
(
$items
,
$last
+
1
,
$r
,
$cur_level
+
1
),
)
+
$items
[
$last
];
return
$nodes
;
}
/**
* See additional notes in @{method:markupText}.
*/
private
function
adjustTreeStyleInformation
(
array
&
$tree
)
{
// The effect here is just to walk backward through the nodes at this level
// and apply the first style in the list to any empty nodes we inserted
// before it. As we go, also recurse down the tree.
$style
=
'-'
;
for
(
$ii
=
count
(
$tree
)
-
1
;
$ii
>=
0
;
$ii
--)
{
if
(
$tree
[
$ii
][
'style'
]
!==
null
)
{
// This is the earliest node we've seen with style, so set the
// style to its style.
$style
=
$tree
[
$ii
][
'style'
];
}
else
{
// This node has no style, so apply the current style.
$tree
[
$ii
][
'style'
]
=
$style
;
}
if
(
$tree
[
$ii
][
'items'
])
{
$this
->
adjustTreeStyleInformation
(
$tree
[
$ii
][
'items'
]);
}
}
}
/**
* See additional notes in @{method:markupText}.
*/
private
function
renderTree
(
array
$tree
,
$level
,
$has_marks
)
{
$style
=
idx
(
head
(
$tree
),
'style'
);
$out
=
array
();
if
(!
$this
->
getEngine
()->
isTextMode
())
{
switch
(
$style
)
{
case
'#'
:
$tag
=
'ol'
;
break
;
case
'-'
:
$tag
=
'ul'
;
break
;
}
if
(
$has_marks
)
{
$out
[]
=
hsprintf
(
'<%s class="remarkup-list remarkup-list-with-checkmarks">'
,
$tag
);
}
else
{
$out
[]
=
hsprintf
(
'<%s class="remarkup-list">'
,
$tag
);
}
$out
[]
=
"
\n
"
;
}
$number
=
1
;
foreach
(
$tree
as
$item
)
{
if
(
$this
->
getEngine
()->
isTextMode
())
{
if
(
$item
[
'text'
]
===
null
)
{
// Don't render anything.
}
else
{
$out
[]
=
str_repeat
(
' '
,
2
*
$level
);
if
(
$item
[
'mark'
]
!==
null
)
{
if
(
$item
[
'mark'
])
{
$out
[]
=
'[X] '
;
}
else
{
$out
[]
=
'[ ] '
;
}
}
else
{
switch
(
$style
)
{
case
'#'
:
$out
[]
=
$number
.
'. '
;
$number
++;
break
;
case
'-'
:
$out
[]
=
'- '
;
break
;
}
}
$out
[]
=
$this
->
applyRules
(
$item
[
'text'
]).
"
\n
"
;
}
}
else
{
if
(
$item
[
'text'
]
===
null
)
{
$out
[]
=
hsprintf
(
'<li class="remarkup-list-item phantom-item">'
);
}
else
{
if
(
$item
[
'mark'
]
!==
null
)
{
if
(
$item
[
'mark'
]
==
true
)
{
$out
[]
=
hsprintf
(
'<li class="remarkup-list-item remarkup-checked-item">'
);
}
else
{
$out
[]
=
hsprintf
(
'<li class="remarkup-list-item remarkup-unchecked-item">'
);
}
$out
[]
=
phutil_tag
(
'input'
,
array
(
'type'
=>
'checkbox'
,
'checked'
=>
(
$item
[
'mark'
]
?
'checked'
:
null
),
'disabled'
=>
'disabled'
,
));
$out
[]
=
' '
;
}
else
{
$out
[]
=
hsprintf
(
'<li class="remarkup-list-item">'
);
}
$out
[]
=
$this
->
applyRules
(
$item
[
'text'
]);
}
}
if
(
$item
[
'items'
])
{
$subitems
=
$this
->
renderTree
(
$item
[
'items'
],
$level
+
1
,
$has_marks
);
foreach
(
$subitems
as
$i
)
{
$out
[]
=
$i
;
}
}
if
(!
$this
->
getEngine
()->
isTextMode
())
{
$out
[]
=
hsprintf
(
"</li>
\n
"
);
}
}
if
(!
$this
->
getEngine
()->
isTextMode
())
{
switch
(
$style
)
{
case
'#'
:
$out
[]
=
hsprintf
(
'</ol>'
);
break
;
case
'-'
:
$out
[]
=
hsprintf
(
'</ul>'
);
break
;
}
}
return
$out
;
}
}
Event Timeline
Log In to Comment