~ubuntu-branches/ubuntu/vivid/phabricator/vivid-proposed

« back to all changes in this revision

Viewing changes to libphutil/src/markup/engine/PhutilRemarkupEngine.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-10-23 20:49:26 UTC
  • mfrom: (0.2.1) (0.1.1)
  • Revision ID: package-import@ubuntu.com-20141023204926-vq80u1op4df44azb
Tags: 0~git20141023-1
Initial release (closes: #703046)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
final class PhutilRemarkupEngine extends PhutilMarkupEngine {
 
4
 
 
5
  const MODE_DEFAULT = 0;
 
6
  const MODE_TEXT = 1;
 
7
 
 
8
  const MAX_CHILD_DEPTH = 32;
 
9
 
 
10
  private $blockRules = array();
 
11
  private $config = array();
 
12
  private $mode;
 
13
  private $metadata = array();
 
14
  private $states = array();
 
15
  private $postprocessRules = array();
 
16
 
 
17
  public function setConfig($key, $value) {
 
18
    $this->config[$key] = $value;
 
19
    return $this;
 
20
  }
 
21
 
 
22
  public function getConfig($key, $default = null) {
 
23
    return idx($this->config, $key, $default);
 
24
  }
 
25
 
 
26
  public function setMode($mode) {
 
27
    $this->mode = $mode;
 
28
    return $this;
 
29
  }
 
30
 
 
31
  public function isTextMode() {
 
32
    return $this->mode & self::MODE_TEXT;
 
33
  }
 
34
 
 
35
  public function setBlockRules(array $rules) {
 
36
    assert_instances_of($rules, 'PhutilRemarkupBlockRule');
 
37
 
 
38
    $rules = msort($rules, 'getPriority');
 
39
 
 
40
    $this->blockRules = $rules;
 
41
    foreach ($this->blockRules as $rule) {
 
42
      $rule->setEngine($this);
 
43
    }
 
44
 
 
45
    $post_rules = array();
 
46
    foreach ($this->blockRules as $block_rule) {
 
47
      foreach ($block_rule->getMarkupRules() as $rule) {
 
48
        $key = $rule->getPostprocessKey();
 
49
        if ($key !== null) {
 
50
          $post_rules[$key] = $rule;
 
51
        }
 
52
      }
 
53
    }
 
54
 
 
55
    $this->postprocessRules = $post_rules;
 
56
 
 
57
    return $this;
 
58
  }
 
59
 
 
60
  public function getTextMetadata($key, $default = null) {
 
61
    if (isset($this->metadata[$key])) {
 
62
      return $this->metadata[$key];
 
63
    }
 
64
    return idx($this->metadata, $key, $default);
 
65
  }
 
66
 
 
67
  public function setTextMetadata($key, $value) {
 
68
    $this->metadata[$key] = $value;
 
69
    return $this;
 
70
  }
 
71
 
 
72
  public function storeText($text) {
 
73
    if ($this->isTextMode()) {
 
74
      $text = phutil_safe_html($text);
 
75
    }
 
76
    return $this->storage->store($text);
 
77
  }
 
78
 
 
79
  public function overwriteStoredText($token, $new_text) {
 
80
    if ($this->isTextMode()) {
 
81
      $new_text = phutil_safe_html($new_text);
 
82
    }
 
83
    $this->storage->overwrite($token, $new_text);
 
84
    return $this;
 
85
  }
 
86
 
 
87
  public function markupText($text) {
 
88
    return $this->postprocessText($this->preprocessText($text));
 
89
  }
 
90
 
 
91
  public function pushState($state) {
 
92
    if (empty($this->states[$state])) {
 
93
      $this->states[$state] = 0;
 
94
    }
 
95
    $this->states[$state]++;
 
96
    return $this;
 
97
  }
 
98
 
 
99
  public function popState($state) {
 
100
    if (empty($this->states[$state])) {
 
101
      throw new Exception("State '{$state}' pushed more than popped!");
 
102
    }
 
103
    $this->states[$state]--;
 
104
    if (!$this->states[$state]) {
 
105
      unset($this->states[$state]);
 
106
    }
 
107
    return $this;
 
108
  }
 
109
 
 
110
  public function getState($state) {
 
111
    return !empty($this->states[$state]);
 
112
  }
 
113
 
 
114
  public function preprocessText($text) {
 
115
    $this->metadata = array();
 
116
    $this->storage = new PhutilRemarkupBlockStorage();
 
117
 
 
118
    $blocks = $this->splitTextIntoBlocks($text);
 
119
 
 
120
    $output = array();
 
121
    foreach ($blocks as $block) {
 
122
      $output[] = $this->markupBlock($block);
 
123
    }
 
124
    $output = $this->flattenOutput($output);
 
125
 
 
126
    $map = $this->storage->getMap();
 
127
    unset($this->storage);
 
128
    $metadata = $this->metadata;
 
129
 
 
130
 
 
131
    return array(
 
132
      'output'    => $output,
 
133
      'storage'   => $map,
 
134
      'metadata'  => $metadata,
 
135
    );
 
136
  }
 
137
 
 
138
  private function splitTextIntoBlocks($text, $depth = 0) {
 
139
    // Apply basic block and paragraph normalization to the text. NOTE: We don't
 
140
    // strip trailing whitespace because it is semantic in some contexts,
 
141
    // notably inlined diffs that the author intends to show as a code block.
 
142
    $text = phutil_split_lines($text, true);
 
143
    $block_rules = $this->blockRules;
 
144
    $blocks = array();
 
145
    $cursor = 0;
 
146
    $prev_block = array();
 
147
 
 
148
    while (isset($text[$cursor])) {
 
149
      $starting_cursor = $cursor;
 
150
      foreach ($block_rules as $block_rule) {
 
151
        $num_lines = $block_rule->getMatchingLineCount($text, $cursor);
 
152
 
 
153
        if ($num_lines) {
 
154
          if ($blocks) {
 
155
            $prev_block = last($blocks);
 
156
          }
 
157
 
 
158
          $curr_block = array(
 
159
            'start' => $cursor,
 
160
            'num_lines' => $num_lines,
 
161
            'rule' => $block_rule,
 
162
            'is_empty' => self::isEmptyBlock($text, $cursor, $num_lines),
 
163
            'children' => array(),
 
164
          );
 
165
 
 
166
          if ($prev_block
 
167
            && self::shouldMergeBlocks($text, $prev_block, $curr_block)) {
 
168
            $blocks[last_key($blocks)]['num_lines'] += $curr_block['num_lines'];
 
169
            $blocks[last_key($blocks)]['is_empty'] =
 
170
              $blocks[last_key($blocks)]['is_empty'] && $curr_block['is_empty'];
 
171
          } else {
 
172
            $blocks[] = $curr_block;
 
173
          }
 
174
 
 
175
          $cursor += $num_lines;
 
176
          break;
 
177
        }
 
178
      }
 
179
 
 
180
      if ($starting_cursor === $cursor) {
 
181
        throw new Exception('Block in text did not match any block rule.');
 
182
      }
 
183
    }
 
184
 
 
185
    foreach ($blocks as $key => $block) {
 
186
      $lines = array_slice($text, $block['start'], $block['num_lines']);
 
187
      $blocks[$key]['text'] = implode('', $lines);
 
188
    }
 
189
 
 
190
    // Stop splitting child blocks apart if we get too deep. This arrests
 
191
    // any blocks which have looping child rules, and stops the stack from
 
192
    // exploding if someone writes a hilarious comment with 5,000 levels of
 
193
    // quoted text.
 
194
 
 
195
    if ($depth < self::MAX_CHILD_DEPTH) {
 
196
      foreach ($blocks as $key => $block) {
 
197
        $rule = $block['rule'];
 
198
        if (!$rule->supportsChildBlocks()) {
 
199
          continue;
 
200
        }
 
201
 
 
202
        list($parent_text, $child_text) = $rule->extractChildText(
 
203
          $block['text']);
 
204
        $blocks[$key]['text'] = $parent_text;
 
205
        $blocks[$key]['children'] = $this->splitTextIntoBlocks(
 
206
          $child_text,
 
207
          $depth + 1);
 
208
      }
 
209
    }
 
210
 
 
211
    return $blocks;
 
212
  }
 
213
 
 
214
  private function markupBlock(array $block) {
 
215
    $children = array();
 
216
    foreach ($block['children'] as $child) {
 
217
      $children[] = $this->markupBlock($child);
 
218
    }
 
219
 
 
220
    if ($children) {
 
221
      $children = $this->flattenOutput($children);
 
222
    } else {
 
223
      $children = null;
 
224
    }
 
225
 
 
226
    return $block['rule']->markupText($block['text'], $children);
 
227
  }
 
228
 
 
229
  private function flattenOutput(array $output) {
 
230
    if ($this->isTextMode()) {
 
231
      $output = implode("\n\n", $output)."\n";
 
232
    } else {
 
233
      $output = phutil_implode_html("\n\n", $output);
 
234
    }
 
235
 
 
236
    return $output;
 
237
  }
 
238
 
 
239
  private static function shouldMergeBlocks($text, $prev_block, $curr_block) {
 
240
    $block_rules = ipull(array($prev_block, $curr_block), 'rule');
 
241
 
 
242
    $default_rule = 'PhutilRemarkupDefaultBlockRule';
 
243
    try {
 
244
      assert_instances_of($block_rules, $default_rule);
 
245
 
 
246
      // If the last block was empty keep merging
 
247
      if ($prev_block['is_empty']) {
 
248
        return true;
 
249
      }
 
250
 
 
251
      // If this line is blank keep merging
 
252
      if ($curr_block['is_empty']) {
 
253
        return true;
 
254
      }
 
255
 
 
256
      // If the current line and the last line have content, keep merging
 
257
      if (strlen(trim($text[$curr_block['start'] - 1]))) {
 
258
        if (strlen(trim($text[$curr_block['start']]))) {
 
259
          return true;
 
260
        }
 
261
      }
 
262
    } catch (Exception $e) {}
 
263
 
 
264
    return false;
 
265
  }
 
266
 
 
267
  private static function isEmptyBlock($text, $start, $num_lines) {
 
268
    for ($cursor = $start; $cursor < $start + $num_lines; $cursor++) {
 
269
      if (strlen(trim($text[$cursor]))) {
 
270
        return false;
 
271
      }
 
272
    }
 
273
    return true;
 
274
  }
 
275
 
 
276
  public function postprocessText(array $dict) {
 
277
    $this->metadata = idx($dict, 'metadata', array());
 
278
 
 
279
    $this->storage = new PhutilRemarkupBlockStorage();
 
280
    $this->storage->setMap(idx($dict, 'storage', array()));
 
281
 
 
282
    foreach ($this->blockRules as $block_rule) {
 
283
      $block_rule->postprocess();
 
284
    }
 
285
 
 
286
    foreach ($this->postprocessRules as $rule) {
 
287
      $rule->didMarkupText();
 
288
    }
 
289
 
 
290
    return $this->restoreText(idx($dict, 'output'), $this->isTextMode());
 
291
  }
 
292
 
 
293
  public function restoreText($text) {
 
294
    return $this->storage->restore($text, $this->isTextMode());
 
295
  }
 
296
}