{{post.title}}
7 | 10 |11 |
├── sample
├── _theme
│ ├── footer.twig
│ ├── index.twig
│ ├── header.twig
│ ├── post.twig
│ └── archives.twig
├── posts
│ └── hello-world.md
├── config.yaml
└── _public
│ └── style.css
├── test.php
├── .gitignore
├── bin
└── logecho
├── composer.json
├── lib
├── functions.php
└── Atom.php
└── workflow
├── import.php
├── main.php
└── compile.php
/sample/_theme/footer.twig:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sample/posts/hello-world.md:
--------------------------------------------------------------------------------
1 | @tag: hello
2 | @category: default
3 | @date:2014-05-31
4 |
5 | Hello World
6 | ===========
7 |
8 | This is your first post.
9 |
--------------------------------------------------------------------------------
/test.php:
--------------------------------------------------------------------------------
1 |
4 | {% for post in index.post %}
5 | {{post.title}}
7 |
10 |
11 |
-------------------------------------------------------------------------------- /sample/_theme/header.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 | 10 |
-------------------------------------------------------------------------------- /sample/_theme/archives.twig: -------------------------------------------------------------------------------- 1 | {% include "header.twig" %} 2 | 3 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/sample/_public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Georgia, "Times New Roman", Times, "STSong","NSimSun",serif;
3 | line-height: 1.6;
4 | color: #333;
5 | }
6 |
7 | a {
8 | color: blue;
9 | text-decoration: none;
10 | }
11 | a:hover {
12 | text-decoration: underline;
13 | }
14 |
15 | h1, h2, h3, h4, h5, h6 {
16 | line-height: 1.25;
17 | }
18 | h1, .h1 { font-size: 32px; }
19 | h2, .h2 { font-size: 28px; }
20 | h3, .h3 { font-size: 24px; }
21 | h4, .h4 { font-size: 18px; }
22 | h5, .h5 { font-size: 16px; }
23 | h6, .h6 { font-size: 14px; }
24 |
25 | pre, code {
26 | background: #F0F0F0;
27 | font-family: Monaco, 'Courier New', Courier, monospace;
28 | }
29 |
30 | pre {
31 | padding: 20px;
32 | }
33 |
34 | code {
35 | padding: 0 5px;
36 | }
37 |
38 | img {
39 | max-width: 100%;
40 | }
41 |
42 |
43 | /**
44 | * layout
45 | */
46 | .container {
47 | margin: 0 auto;
48 | max-width: 1140px;
49 | }
50 |
51 | .col-main,
52 | .col-secondary {
53 | float: left;
54 | padding: 15px;
55 | -webkit-box-sizing: border-box;
56 | -moz-box-sizing: border-box;
57 | box-sizing: border-box;
58 | }
59 |
60 | .col-main {
61 | padding-left: 0;
62 | border-left: 1px solid #eee;
63 | width: 75%;
64 | }
65 |
66 | .col-secondary {
67 | padding-right: 40px;
68 | width: 25%;
69 | }
70 |
71 |
72 | /**
73 | * navigation
74 | */
75 | .site-header {
76 | margin-top: 40px;
77 | margin-bottom: 40px;
78 | }
79 | .site-logo {
80 | font-size: 64px;
81 | background-color: #333;
82 | color: #fff;
83 | display: inline-block;
84 | text-align: center;
85 | line-height: 128px;
86 | width: 128px;
87 | height: 128px;
88 | border-radius: 128px;
89 | border: 3px solid #fff;
90 | box-shadow: 0 0 0 5px #333;
91 | }
92 | .site-logo:hover {
93 | text-decoration: none;
94 | }
95 |
96 | .site-name {
97 | margin: 1em 0 -0.4em;
98 | }
99 | .site-name a {
100 | color: #333;
101 | }
102 | .site-bio {
103 | color: #999;
104 | }
105 | .site-nav {
106 | padding: 0;
107 | list-style: none;
108 | }
109 | .site-nav li {
110 | margin: 10px 0;
111 | }
112 |
113 | .site-nav a {
114 | display: inline-block;
115 | padding: 2px 10px;
116 | border: 3px solid #eee;
117 | border-radius: 50px;
118 | }
119 |
120 | .site-nav a:hover {
121 | border-color: blue;
122 | text-decoration: none;
123 | }
124 |
125 |
126 | /**
127 | * post
128 | */
129 | .post {
130 | padding: 20px 0 20px 40px;
131 | border-bottom: 1px solid #eee;
132 | }
133 | .post__title {
134 | margin-bottom: 5px;
135 | }
136 | .post__title a {
137 | color: #333;
138 | }
139 | .post__title a:hover {
140 | }
141 | .post__meta {
142 | color: #999;
143 | }
144 | .post__tag {
145 | margin: 1em 0;
146 | }
147 | .post__tag ul {
148 | display: inline;
149 | padding: 0;
150 | }
151 | .post__tag li {
152 | display: inline-block;
153 | color: #999;
154 | }
155 | .post__tag li:after {
156 | content: ",";
157 | }
158 | .post__tag li:last-child:after {
159 | content: none;
160 | }
161 |
162 | /* page */
163 | .page {
164 | border-bottom: none;
165 | }
166 | .hr-title {
167 | margin: 20px 0;
168 | border: 1px solid #eee;
169 | border-width: 1px 0 0 0;
170 | }
171 |
172 |
173 | .btn-archive {
174 | display: block;
175 | padding: 40px 0 40px 40px;
176 | text-align: center;
177 | font-size: 18px;
178 | }
179 |
180 | .site-footer {
181 | padding: 20px 0 20px 40px;
182 | border-top: 1px solid #eee;
183 | text-align: center;
184 | font-style: italic;
185 | }
186 |
187 |
188 | /**
189 | * clearfix
190 | */
191 | .clearfix:before,
192 | .clearfix:after {
193 | content: " "; // 1
194 | display: table; // 2
195 | }
196 | .clearfix:after {
197 | clear: both;
198 | }
--------------------------------------------------------------------------------
/lib/functions.php:
--------------------------------------------------------------------------------
1 | '33',
23 | 'done' => '32',
24 | 'error' => '31',
25 | 'debug' => '36'
26 | ];
27 |
28 | if (!__DEBUG__ && $type == 'debug') {
29 | return;
30 | }
31 |
32 | $color = isset($colors[$type]) ? $colors[$type] : '37';
33 |
34 | $args = array_slice(func_get_args(), 2);
35 | array_unshift($args, $str);
36 | $str = call_user_func_array('sprintf', $args);
37 |
38 | echo "\033[{$color};1m[" . $type . "]\t\033[37;0m {$str}\n";
39 | }
40 |
41 | /**
42 | * trigger a fatal error
43 | *
44 | * @param string $str
45 | * @throws Exception
46 | */
47 | function le_fatal($str) {
48 | $args = func_get_args();
49 | throw new Exception(call_user_func_array('sprintf', $args));
50 | }
51 |
52 | // init global vars
53 | global $workflow, $context;
54 | $workflow = [];
55 | $context = new stdClass();
56 |
57 | /**
58 | * get current caller file
59 | *
60 | * @return string
61 | */
62 | function le_get_current_namespace() {
63 | static $file;
64 |
65 | $traces = debug_backtrace(!DEBUG_BACKTRACE_IGNORE_ARGS & !DEBUG_BACKTRACE_PROVIDE_OBJECT, 2);
66 | $current = array_pop($traces);
67 |
68 | $file = isset($current['file']) ? $current['file'] : $file;
69 |
70 | return pathinfo($file, PATHINFO_FILENAME);
71 | }
72 |
73 | /**
74 | * get a dir recursive iterator
75 | *
76 | * @param string $dir
77 | * @return RecursiveIteratorIterator
78 | */
79 | function le_get_all_files($dir) {
80 | return !is_dir($dir) ? [] : new RecursiveIteratorIterator(new LEIgnorantRecursiveDirectoryIterator($dir,
81 | FilesystemIterator::KEY_AS_FILENAME
82 | | FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS));
83 | }
84 |
85 | class LEIgnorantRecursiveDirectoryIterator extends RecursiveDirectoryIterator {
86 | function getChildren() {
87 | try {
88 | return new LEIgnorantRecursiveDirectoryIterator($this->getPathname(), FilesystemIterator::KEY_AS_FILENAME
89 | | FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS);
90 | } catch(UnexpectedValueException $e) {
91 | return new RecursiveArrayIterator(array());
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * add workflow
98 | *
99 | * @param string $name
100 | * @param mixed $func
101 | */
102 | function le_add_workflow($name, $func) {
103 | global $workflow;
104 |
105 | $ns = le_get_current_namespace();
106 | $workflow[$ns . '.' . $name] = $func;
107 | }
108 |
109 | /**
110 | * @param $name
111 | * @return mixed
112 | */
113 | function le_do_workflow($name) {
114 | global $workflow, $context;
115 |
116 | $args = func_get_args();
117 | array_shift($args);
118 |
119 | $parts = explode('.', $name, 2);
120 | if (2 == count($parts)) {
121 | list ($ns) = $parts;
122 | } else {
123 | $ns = le_get_current_namespace();
124 | $name = $ns . '.' . $name;
125 | }
126 |
127 | require_once __DIR__ . '/../workflow/' . $ns . '.php';
128 |
129 | if (!isset($workflow[$name])) {
130 | le_fatal('can not find workflow "%s"', $name);
131 | }
132 |
133 | $desc = implode(', ', array_map(function ($arg) {
134 | return is_string($arg) ? mb_strimwidth(
135 | str_replace(["\r", "\n"], '', $arg)
136 | , 0, 10, '...', 'UTF-8') : '...';
137 | }, $args));
138 |
139 | le_console('debug', '%s%s', $name, empty($desc) ? '' : ': ' . $desc);
140 | return call_user_func_array($workflow[$name], $args);
141 | }
142 |
143 |
--------------------------------------------------------------------------------
/lib/Atom.php:
--------------------------------------------------------------------------------
1 | _charset = $charset;
81 | $this->_special = array_map('chr', range(0, 8));
82 | }
83 |
84 | /**
85 | * 去掉一些特殊字符
86 | *
87 | * @param mixed $str
88 | * @access private
89 | * @return string
90 | */
91 | private function encode($str)
92 | {
93 | return str_replace($this->_special, '', htmlspecialchars($str, ENT_IGNORE, 'UTF-8'));
94 | }
95 |
96 | /**
97 | * 设置标题
98 | *
99 | * @access public
100 | * @param string $title 标题
101 | * @return void
102 | */
103 | public function setTitle($title)
104 | {
105 | $this->_title = $title;
106 | }
107 |
108 | /**
109 | * 设置副标题
110 | *
111 | * @access public
112 | * @param string $subTitle 副标题
113 | * @return void
114 | */
115 | public function setSubTitle($subTitle)
116 | {
117 | $this->_subTitle = $subTitle;
118 | }
119 |
120 | /**
121 | * 设置聚合地址
122 | *
123 | * @access public
124 | * @param string $feedUrl 聚合地址
125 | * @return void
126 | */
127 | public function setFeedUrl($feedUrl)
128 | {
129 | $this->_feedUrl = $feedUrl;
130 | }
131 |
132 | /**
133 | * 设置主页
134 | *
135 | * @access public
136 | * @param string $baseUrl 主页地址
137 | * @return void
138 | */
139 | public function setBaseUrl($baseUrl)
140 | {
141 | $this->_baseUrl = $baseUrl;
142 | }
143 |
144 | /**
145 | * $item的格式为
146 | *
147 | * array (
148 | * 'title' => 'xxx',
149 | * 'content' => 'xxx',
150 | * 'excerpt' => 'xxx',
151 | * 'date' => 'xxx',
152 | * 'link' => 'xxx',
153 | * 'author' => 'xxx',
154 | * 'comments' => 'xxx',
155 | * )
156 | *
157 | *
158 | * @access public
159 | * @param array $item
160 | * @return unknown
161 | */
162 | public function addItem(array $item)
163 | {
164 | $this->_items[] = $item;
165 | }
166 |
167 | /**
168 | * 输出字符串
169 | *
170 | * @access public
171 | * @return string
172 | */
173 | public function generate()
174 | {
175 | $result = '_charset . '"?>' . self::EOL;
176 |
177 | $result .= '
", $text); 231 | $text = preg_replace_callback("/
]*>]*>(.+?)<\/code><\/pre>/is", function ($matches) {
232 | return '' . $matches[1] . '
';
233 | }, $text);
234 | $text = preg_replace_callback("/]*>(.+?)<\/code>/is", function ($matches) {
235 | if (false !== strpos($matches[1], "\n")) {
236 | return '' . $matches[1] . '
';
237 | }
238 |
239 | return '' . $matches[1] . '';
240 | }, $text);
241 | $text = preg_replace("/]+(href=\"[^\"]+\")[^>]*>/is", "", $text);
242 | $text = preg_replace("/
]+(src=\"[^\"]+\")[^>]*\/?>/is", "
", $text);
243 | $parser = new \Markdownify\ConverterExtra();
244 |
245 | $content = "\n" . $post['title'] . "\n"
246 | . str_repeat('=', strlen($post['title'])) . "\n\n"
247 | . $parser->parseString($text);
248 |
249 | $date = $post['dateCreated'];
250 | if (!empty($post['mt_keywords'])) {
251 | $content = "@tag:{$post['mt_keywords']}\n" . $content;
252 | }
253 |
254 | if (!empty($post['categories'])) {
255 | $categories = [];
256 | foreach ($post['categories'] as $category) {
257 | $found = array_search($category, $categoriesConfig);
258 | if (false !== $found) {
259 | $categories[] = $found;
260 | }
261 | }
262 |
263 | if (!empty($categories)) {
264 | $content = "@category:" . implode(',', $categories) . "\n" . $content;
265 | }
266 | }
267 |
268 | $content = "@date:{$date->year}-{$date->month}-{$date->day} {$date->hour}:{$date->minute}:{$date->second}\n"
269 | . $content;
270 | $content = "@slug:{$post['wp_slug']}\n" . $content;
271 |
272 | return $content;
273 | });
274 |
--------------------------------------------------------------------------------
/workflow/main.php:
--------------------------------------------------------------------------------
1 | 'Create an empty Logecho directory',
23 | 'build' => 'Build contents to _target directory',
24 | 'sync' => 'Sync _target by using your sync config',
25 | 'serve' => 'Start a http server to watch your site',
26 | 'watch' => '',
27 | 'archive' => '',
28 | 'help' => 'Show help documents',
29 | 'import' => 'Import data from other blogging platform which is using xmlrpc'
30 | ];
31 |
32 | if (count($argv) > 0 && $argv[0] == $_SERVER['PHP_SELF']) {
33 | array_shift($argv);
34 | }
35 |
36 | if (count($argv) == 0) {
37 | $argv[] = 'help';
38 | }
39 |
40 | $help = function () use ($opts) {
41 | echo "usage: logecho \n\n";
42 | echo "Here are the most commonly used logecho commands:\n";
43 |
44 | foreach ($opts as $name => $words) {
45 | $name = str_pad($name, 12, ' ', STR_PAD_RIGHT);
46 | echo " {$name}{$words}\n";
47 | }
48 | };
49 |
50 | $name = array_shift($argv);
51 | if (!isset($opts[$name])) {
52 | le_console('error', 'can not handle %s command, please use the following commands', $name);
53 | $help();
54 | exit(1);
55 | }
56 |
57 | if ('help' == $name) {
58 | $help();
59 | } else {
60 | if ($name != 'update') {
61 | if (count($argv) < 1) {
62 | le_fatal('a blog directory is required');
63 | }
64 |
65 | list ($dir) = $argv;
66 | if (!is_dir($dir)) {
67 | le_fatal('blog directory "%s" is not exists', $dir);
68 | }
69 |
70 | $context->dir = rtrim($dir, '/') . '/';
71 | $context->cmd = __DEBUG__ ? $_SERVER['_'] . ' ' . $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'];
72 |
73 | if ($name != 'init') {
74 | le_do_workflow('read_config');
75 | }
76 | }
77 |
78 | array_unshift($argv, $name);
79 | call_user_func_array('le_do_workflow', $argv);
80 |
81 | le_console('done', $name);
82 | }
83 | });
84 |
85 | // read config
86 | le_add_workflow('read_config', function () use ($context) {
87 | $file = $context->dir . 'config.yaml';
88 | if (!file_exists($file)) {
89 | le_fatal('can not find config file "%s"', $file);
90 | }
91 |
92 | $config = Spyc::YAMLLoad($file);
93 | if (!$config) {
94 | le_fatal('config file is not a valid yaml file');
95 | }
96 |
97 | $context->config = $config;
98 | });
99 |
100 | // sync directory
101 | le_add_workflow('sync', function ($source = null, $target = null) use ($context) {
102 | $url = $context->config['sync'];
103 | if (empty($url)) {
104 | le_fatal('Missing sync url configure');
105 | }
106 |
107 | le_do_workflow('build');
108 | $source = $context->dir . '_target';
109 | $img = tempnam(sys_get_temp_dir(), 'le');
110 | $data = '';
111 |
112 | // compress all files
113 | $files = le_get_all_files($source);
114 | $offset = strlen($source);
115 | $first = true;
116 |
117 | foreach ($files as $file => $path) {
118 | if ($file[0] == '.') {
119 | continue;
120 | }
121 |
122 | $original = substr($path, $offset);
123 | $data .= ($first ? '' : "\n") . $original . ' ' . base64_encode(file_get_contents($path));
124 | $first = false;
125 | }
126 |
127 | $ch = curl_init();
128 |
129 | curl_setopt_array($ch, [
130 | CURLOPT_URL => $url,
131 | CURLOPT_RETURNTRANSFER => true,
132 | CURLOPT_HEADER => false,
133 | CURLOPT_SSL_VERIFYPEER => false,
134 | CURLOPT_SSL_VERIFYHOST => false,
135 | CURLOPT_TIMEOUT => 20,
136 | CURLOPT_POST => true,
137 | CURLOPT_POSTFIELDS => $data
138 | ]);
139 |
140 | $response = curl_exec($ch);
141 | if (false === $response) {
142 | le_fatal(curl_error($ch));
143 | }
144 |
145 | $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
146 | if (200 != $code) {
147 | le_fatal($code);
148 | }
149 | });
150 |
151 | // build all
152 | le_add_workflow('build', function () use ($context) {
153 | le_do_workflow('compile.init');
154 | le_do_workflow('compile.compile');
155 |
156 | $source = $context->dir . '_public';
157 | $target = $context->dir . '_target/public';
158 |
159 | // delete all files in target
160 | $files = le_get_all_files($target);
161 | $dirs = [];
162 |
163 | foreach ($files as $file => $path) {
164 | $dir = dirname($path);
165 |
166 | // do not remove root directory
167 | if (!in_array($dir, $dirs) && realpath($dir) != realpath($target)) {
168 | $dirs[] = $dir;
169 | }
170 |
171 | // remove all files first
172 | if (!unlink($path)) {
173 | le_fatal('can not unlink file %s, permission denied', $path);
174 | }
175 | }
176 |
177 | // remove all dirs
178 | $dirs = array_reverse($dirs);
179 | foreach ($dirs as $dir) {
180 | if (!rmdir($dir)) {
181 | le_fatal('can not rm directory %s, permission denied', $dir);
182 | }
183 | }
184 |
185 | // copy all files
186 | $files = le_get_all_files($source);
187 | $offset = strlen($source);
188 |
189 | foreach ($files as $file => $path) {
190 | if ($file[0] == '.') {
191 | continue;
192 | }
193 |
194 | $original = substr($path, $offset);
195 | $current = $target . '/' . $original;
196 | $dir = dirname($current);
197 |
198 | if (!is_dir($dir)) {
199 | if (!mkdir($dir, 0755, true)) {
200 | le_fatal('can not make directory %s, permission denied', $dir);
201 | }
202 | }
203 |
204 | copy($path, $current);
205 | }
206 | });
207 |
208 | // init
209 | le_add_workflow('init', function () use ($context) {
210 | if (file_exists($context->dir . 'config.yaml')) {
211 | $confirm = readline('target dir is not empty, continue? (Y/n) ');
212 |
213 | if (strtolower($confirm) != 'y') {
214 | exit;
215 | }
216 | }
217 |
218 | $dir = __DIR__ . '/../sample';
219 | $offset = strlen($dir);
220 |
221 | $files = le_get_all_files($dir);
222 |
223 | foreach ($files as $file => $path) {
224 | if ($file[0] == '.') {
225 | continue;
226 | }
227 |
228 | $original = substr($path, $offset);
229 | $target = $context->dir . $original;
230 | $dir = dirname($target);
231 |
232 | if (!is_dir($dir)) {
233 | if (!mkdir($dir, 0755, true)) {
234 | le_fatal('can not make directory %s, permission denied', $dir);
235 | }
236 | }
237 |
238 | copy($path, $target);
239 | }
240 | });
241 |
242 | // serve
243 | le_add_workflow('serve', function () use ($context) {
244 | $target = $context->dir . '_target';
245 | if (!is_dir($target)) {
246 | le_console('info', 'building target files, please wait ...');
247 | exec($context->cmd . ' build ' . $context->dir);
248 | }
249 |
250 | $proc = proc_open($context->cmd . ' watch ' . $context->dir, [
251 | 0 => ['pipe', 'r'],
252 | 1 => ['pipe', 'w'],
253 | 2 => ['file', sys_get_temp_dir() . '/logecho-error.log', 'a']
254 | ], $pipes, getcwd());
255 | stream_set_blocking($pipes[0], 0);
256 | stream_set_blocking($pipes[1], 0);
257 |
258 | le_console('info', 'Listening on localhost:7000');
259 | le_console('info', 'Document root is %s', $target);
260 | le_console('info', 'Press Ctrl-C to quit');
261 | exec('/usr/bin/env php -S localhost:7000 -t ' . $target);
262 | });
263 |
264 | // archive
265 | le_add_workflow('archive', function () use ($context) {
266 | // init complier
267 | le_do_workflow('compile.init');
268 |
269 | foreach ($context->config['blocks'] as $type => $block) {
270 | if (!isset($block['source']) || !is_string($block['source'])) {
271 | continue;
272 | }
273 |
274 | $source = trim($block['source'], '/');
275 | $files = glob($context->dir . '/' . $source . '/*.md');
276 | $list = [];
277 |
278 | foreach ($files as $file) {
279 | list ($metas) = le_do_workflow('compile.get_metas', $file);
280 | $date = $metas['date'];
281 |
282 | $list[$file] = $date;
283 | }
284 |
285 | asort($list);
286 | $index = 1;
287 |
288 | foreach ($list as $file => $date) {
289 | $info = pathinfo($file);
290 | $fileName = $info['filename'];
291 | $dir = $info['dirname'];
292 |
293 | if (preg_match("/^[0-9]{4}\.(.+)$/", $fileName, $matches)) {
294 | $fileName = $matches[1];
295 | }
296 |
297 | $source = realpath($file);
298 | $target = rtrim($dir, '/') . '/' . str_pad($index, 4, '0', STR_PAD_LEFT) . '.' . $fileName . '.md';
299 |
300 | if ($source != $target && !file_exists($target)) {
301 | le_console('info', basename($source) . ' => ' . basename($target));
302 | rename($source, $target);
303 | }
304 |
305 | $index ++;
306 | }
307 | }
308 | });
309 |
310 | // watch
311 | le_add_workflow('watch', function () use ($context) {
312 | $lastSum = '';
313 |
314 | while (true) {
315 | // get sources
316 | $sources = ["\/_theme\/", "\/_public\/"];
317 | $sum = '';
318 |
319 | foreach ($context->config['blocks'] as $type => $block) {
320 | if (!isset($block['source']) || !is_string($block['source'])) {
321 | continue;
322 | }
323 |
324 | $source = trim($block['source'], '/');
325 | $source = empty($source) ? '/' : '/' . $source . '/';
326 |
327 | $sources[] = preg_quote($source, '/');
328 | }
329 |
330 | if (!empty($sources)) {
331 | $regex = "/^" . preg_quote(rtrim($context->dir, '/'))
332 | . "(" . implode('|', $sources) . ")/";
333 |
334 | $files = le_get_all_files($context->dir);
335 |
336 | foreach ($files as $file => $path) {
337 | if (!preg_match($regex, $path) || $file[0] == '.') {
338 | continue;
339 | }
340 |
341 | $sum .= md5_file($path);
342 | }
343 |
344 | $sum = md5($sum . md5_file($context->dir . 'config.yaml'));
345 | if ($lastSum != $sum) {
346 | exec($context->cmd . ' build ' . $context->dir);
347 | $lastSum = $sum;
348 | }
349 | }
350 |
351 | sleep(1);
352 | }
353 | });
354 |
355 | // import
356 | le_add_workflow('import', function () use ($context) {
357 | le_do_workflow('import.init');
358 | });
359 |
--------------------------------------------------------------------------------
/workflow/compile.php:
--------------------------------------------------------------------------------
1 | indexConfig = [];
8 | $context->metas = [];
9 | $context->data = [];
10 | $context->index = [];
11 | $context->cached = [];
12 | $context->sitemap = [];
13 |
14 | if (isset($context->config['blocks']['index'])) {
15 | $context->indexConfig = $context->config['blocks']['index'];
16 | unset($context->config['blocks']['index']);
17 | }
18 |
19 | // init twig
20 | $loader = new Twig_Loader_Filesystem($context->dir . '_theme');
21 | $context->template = new Twig_Environment($loader, [
22 | 'autoescape' => false
23 | ]);
24 |
25 | le_do_workflow('init_twig', $context->template);
26 |
27 | // read config
28 | le_do_workflow('read_metas');
29 | le_do_workflow('read_globals');
30 | });
31 |
32 | // load extension
33 | le_add_workflow('init_twig', function (Twig_Environment $twig) use ($context) {
34 | $twig->addFilter(new Twig_SimpleFilter('more', function ($str, $limit = 0) {
35 | if ($limit > 0) {
36 | $str = strip_tags($str);
37 | return mb_strlen($str, 'UTF-8') > $limit
38 | ? mb_substr($str, 0, $limit, 'UTF-8') . ' ...' : $str;
39 | }
40 |
41 | $parts = preg_split("//is", $str);
42 | return count($parts) > 1 ? $parts[0] . '...
' : $str;
43 | }));
44 | });
45 |
46 | // read metas
47 | le_add_workflow('read_metas', function () use ($context) {
48 | // read metas from post
49 | foreach ($context->config['blocks'] as $type => $block) {
50 | if (isset($block['source']) && is_string($block['source'])) {
51 | $files = glob($context->dir . $block['source'] . '/*.md');
52 |
53 | foreach ($files as $file) {
54 | list ($metas) = le_do_workflow('get_metas', $file);
55 | $term = pathinfo($file, PATHINFO_FILENAME);
56 |
57 | $context->index[$type][$term] = $metas['date'];
58 |
59 | // get metas group from file
60 | foreach ($metas as $key => $relates) {
61 | if (!is_array($relates)) {
62 | continue;
63 | }
64 |
65 | // link meta and post
66 | foreach ($relates as $relate) {
67 | $context->metas[$key][$relate][] = [$type, $term];
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | $context->index = array_map(function ($index) {
75 | arsort($index);
76 | return array_keys($index);
77 | }, $context->index);
78 |
79 | if (!empty($context->metas['archive'])) {
80 | krsort($context->metas['archive']);
81 | }
82 |
83 | foreach ($context->metas as $type => $relates) {
84 | foreach ($relates as $key => $relate) {
85 | usort($relate, function ($a, $b) use ($context) {
86 | $x = array_search($a[1], $context->index[$a[0]]);
87 | $y = array_search($b[1], $context->index[$b[0]]);
88 |
89 | return $x > $y ? 1 : -1;
90 | });
91 |
92 | $context->metas[$type][$key] = $relate;
93 | }
94 | }
95 | });
96 |
97 | // read globals
98 | le_add_workflow('read_globals', function () use ($context) {
99 | if (isset($context->config['globals'])) {
100 | $context->data = $context->config['globals'];
101 | }
102 |
103 | $context->data['metas'] = [];
104 | foreach ($context->config['blocks'] as $key => $val) {
105 | if (isset($val['source']) && is_string($val['source'])) {
106 | continue;
107 | }
108 |
109 | $context->data['metas'][$key] = [];
110 |
111 | $path = '/' . trim($val['target'], '/');
112 | $url = '/' == substr($val['target'], -1)
113 | ? $path . '/%s.' . (isset($val['ext']) ? $val['ext'] : 'html') : $path . '#' . $key . '-%s';
114 |
115 | if (isset($val['source']) && is_array($val['source'])) {
116 | foreach ($val['source'] as $slug => $name) {
117 | $context->data['metas'][$key][$slug] = [
118 | 'slug' => $slug,
119 | 'name' => $name,
120 | 'url' => sprintf($url, urlencode($slug)),
121 | 'count' => isset($context->metas[$key][$slug]) ? count($context->metas[$key][$slug]) : 0
122 | ];
123 | }
124 | } else if (!isset($val['source']) && !empty($context->metas[$key])) {
125 | foreach ($context->metas[$key] as $slug => $terms) {
126 | $context->data['metas'][$key][$slug] = [
127 | 'slug' => $slug,
128 | 'name' => $slug,
129 | 'url' => sprintf($url, urlencode($slug)),
130 | 'count' => count($terms)
131 | ];
132 | }
133 | }
134 | }
135 |
136 | foreach ($context->data as $key => $val) {
137 | $context->template->addGlobal($key, $val);
138 | }
139 | });
140 |
141 | // get metas
142 | le_add_workflow('get_metas', function ($file) use ($context) {
143 | $str = ltrim(file_get_contents($file));
144 | $metas = [];
145 |
146 | $lines = explode("\n", $str);
147 | foreach ($lines as $index => $line) {
148 | if (preg_match("/^@([_a-z0-9-]+):(.+)$/i", $line, $matches)) {
149 | $key = strtolower($matches[1]);
150 | if ('date' == $key) {
151 | $metas['date'] = strtotime(trim($matches[2]));
152 | continue;
153 | } else if (!isset($context->config['blocks'][$key])) {
154 | $metas[$key] = trim($matches[2]);
155 | continue;
156 | }
157 |
158 | // read block
159 | $block = $context->config['blocks'][$key];
160 | $values = array_map('trim', explode(',', trim($matches[2])));
161 |
162 | foreach ($values as $value) {
163 | if (isset($block['source'])) {
164 | if (is_array($block['source'])) {
165 | // specific by hash object
166 | if (isset($block['source'][$value])) {
167 | $metas[$key][] = $value;
168 | } else if (in_array($value, $block['source'])) {
169 | $metas[$key][] = array_search($value, $block['source']);
170 | }
171 | } else {
172 | $file = $context->dir . $block['source'] . '/' . $value . '.md';
173 | if (file_exists($file)) {
174 | $metas[$key][] = $value;
175 | }
176 | }
177 | } else {
178 | $metas[$key][] = $value;
179 | }
180 | }
181 | } else {
182 | break;
183 | }
184 | }
185 |
186 | if (!isset($metas['date'])) {
187 | $metas['date'] = filemtime($file);
188 | }
189 |
190 | // special archive block
191 | if (!isset($metas['archive']) && isset($context->config['blocks']['archive'])) {
192 | $metas['archive'][] = date('Y.m', $metas['date']);
193 | }
194 |
195 | $str = implode("\n", array_slice($lines, $index));
196 | return [$metas, $str];
197 | });
198 |
199 | // parse
200 | le_add_workflow('parse', function ($text) use ($context) {
201 | static $handler;
202 |
203 | if (empty($handler)) {
204 | $parser = isset($context->parser) ? $context->parser : 'markdown';
205 | $defaults = [
206 | 'markdown' => '\Michelf\MarkdownExtra#defaultTransform',
207 | 'parsedown' => 'ParsedownExtra#text'
208 | ];
209 |
210 | $parser = isset($defaults[$parser]) ? $defaults[$parser] : $parser;
211 |
212 | if (strpos($parser, ':')) {
213 | $parts = explode(':', $parser, 2);
214 | list ($file, $parser) = $parts;
215 | require_once $file;
216 | }
217 |
218 | $method = 'text';
219 | if (strpos($parser, '#')) {
220 | list ($className, $method) = explode('#', $parser);
221 | } else {
222 | $className = $parser;
223 | }
224 |
225 | if (!class_exists($className)) {
226 | le_fatal('can not find parser class "%s"', $className);
227 | }
228 |
229 | $ref = new ReflectionClass($className);
230 | $methodRef = $ref->getMethod($method);
231 |
232 | if (empty($methodRef)) {
233 | le_fatal('can not find method "%s:%s"', $className, $method);
234 | }
235 |
236 | $handler = [$methodRef->isStatic() ? $className : $ref->newInstance(), $method];
237 | }
238 |
239 | return call_user_func($handler, $text);
240 | });
241 |
242 | // get post
243 | le_add_workflow('get_post', function ($type, $key) use ($context) {
244 | $block = $context->config['blocks'][$type];
245 | $file = $context->dir . $block['source'] . '/' . $key . '.md';
246 | list ($result, $text) = le_do_workflow('get_metas', $file);
247 | $base = !empty($context->data['url']) ? rtrim($context->data['url'], '/') : '';
248 |
249 | // expand metas
250 | foreach ($result as $name => &$metas) {
251 | if (!is_array($metas)) {
252 | continue;
253 | }
254 |
255 | if (isset($context->data['metas'][$name])) {
256 | $current = $context->data['metas'][$name];
257 | $metas = array_map(function ($index) use ($current) {
258 | return $current[$index];
259 | }, $metas);
260 | }
261 | }
262 |
263 | $result['type'] = $type;
264 | $result['id'] = $type . ':' . $key;
265 | $result['title'] = $key;
266 | $result['text'] = $text;
267 | $result['content'] = le_do_workflow('parse', $text);
268 | $result['ext'] = isset($block['ext']) ? $block['ext'] : 'html';
269 | if (!isset($result['slug'])) {
270 | $result['slug'] = preg_match("/^[0-9]{4}\.(.+)$/", $key, $matches) ? $matches[1] : $key;
271 | }
272 | $result['url'] = '/' . trim($block['target'], '/')
273 | . '/' . urlencode($result['slug']) . '.' . $result['ext'];
274 | $result['permalink'] = $base . $result['url'];
275 |
276 | $dom = new \DOMDocument();
277 | @$dom->loadHTML('' .
278 | ''
279 | . $result['content'] . '