diff --git a/README.en.md b/README.en.md
index 8e1d41e..4c3abcc 100644
--- a/README.en.md
+++ b/README.en.md
@@ -44,3 +44,5 @@ Who upgraded from a commit BEFORE [d8be150fc6b8c43af61c5e4aca4fc0fe0445aef3](htt
```sql
ALTER TABLE `user` ADD `prefer_formatted_code` TINYINT(1) NOT NULL DEFAULT 1 AFTER `public_email`;
```
+
+Who upgraded from a commit BEFORE [c192e8001ac81cab132ae033b39f09a094587633](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633) (Mar 23, 2019) **MUST** install `redis-server` and [pygments](http://pygments.org/) on the web server. Markdown contents may be broken by switching to new renderer, [migrates/html-table-merge-cell-to-md.js](migrates/html-table-merge-cell-to-md.js) may help the migration。
diff --git a/README.md b/README.md
index a41c390..a0405a4 100644
--- a/README.md
+++ b/README.md
@@ -44,3 +44,5 @@ ALTER TABLE `problem` ADD `publicize_time` DATETIME NOT NULL DEFAULT CURRENT_TIM
```sql
ALTER TABLE `user` ADD `prefer_formatted_code` TINYINT(1) NOT NULL DEFAULT 1 AFTER `public_email`;
```
+
+从该 commit [c192e8001ac81cab132ae033b39f09a094587633](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633)(2019 年 3 月 23 日)前更新的用户**必须**在网站服务器上安装 `redis-server` 与 [pygments](http://pygments.org/)。旧的 Markdown 内容可能因切换到新渲染器被破坏,[migrates/html-table-merge-cell-to-md.js](migrates/html-table-merge-cell-to-md.js) 可能对迁移有所帮助。
diff --git a/migrates/html-table-merge-cell-to-md.js b/migrates/html-table-merge-cell-to-md.js
new file mode 100755
index 0000000..475fe1b
--- /dev/null
+++ b/migrates/html-table-merge-cell-to-md.js
@@ -0,0 +1,127 @@
+/*
+ * This script will help migrate from the old marked-based markdown renderer
+ * to the new markdown-it based (syzoj-renderer).
+ *
+ * The later doesn't support inline markdown inside HTML blocks. But in
+ * LibreOJ that's widely used in problem's limits' cell-merged table displaying
+ * displaying. So TeX maths inside are broken.
+ */
+
+const cheerio = require('cheerio');
+
+function processMarkdown(text) {
+ return text.replace(/(
)/gi, (match, offset, string) => {
+ const $ = cheerio.load(match, { decodeEntities: false }),
+ table = $('table');
+
+ let defaultAlign = '-';
+ if (table.hasClass('center')) defaultAlign = ':-:';
+ else if (table.hasClass('left')) defaultAlign = ':-';
+ else if (table.hasClass('right')) defaultAlign = '-:';
+
+ let columnCount = 0;
+ const columnAlign = [];
+ table.find('th').each((i, th) => {
+ const count = parseInt($(th).attr('colspan')) || 1;
+ columnCount += count;
+
+ const style = ($(th).attr('style') || '').split(' ').join('').toLowerCase();
+ if (style.includes('text-align:center')) columnAlign.push(':-:');
+ else if (style.includes('text-align:left')) columnAlign.push(':-');
+ else if (style.includes('text-align:right')) columnAlign.push('-:');
+ else columnAlign.push(defaultAlign);
+ });
+
+ const rowCount = table.find('tr').length;
+
+ function escape(s) {
+ return ` ${s.trim().split('|').join('\\|')} `;
+ }
+
+ const matrix = Array(rowCount).fill(null).map(() => []);
+ table.find('tr').each((i, tr) => {
+ const cells = $(tr).find('th, td');
+
+ let columnIndex = 0, resColumnIndex = 0;
+ cells.each((j, td) => {
+ while (typeof matrix[i][resColumnIndex] !== 'undefined') resColumnIndex++;
+
+ if (columnIndex >= columnCount) return false;
+ if (resColumnIndex >= columnCount) return false;
+
+ const colspan = parseInt($(td).attr('colspan')) || 1,
+ rowspan = parseInt($(td).attr('rowspan')) || 1,
+ content = $(td).html();
+
+ for (let cntRow = 0; cntRow < rowspan; cntRow++) {
+ for (let cntCol = 0; cntCol < colspan; cntCol++) {
+ if (i + cntRow < rowCount && resColumnIndex + cntCol < columnCount) {
+ matrix[i + cntRow][resColumnIndex + cntCol] = escape(content);
+ }
+ }
+ }
+
+ resColumnIndex += colspan;
+ });
+ });
+
+ const code = [matrix[0], columnAlign, ...matrix.slice(1)].map(row => `|${row.join('|')}|`).join('\n');
+
+ return `\n\n${code}\n\n\n\n`;
+ });
+}
+
+// Load syzoj.
+process.chdir(__dirname + '/..');
+require('..');
+
+const modelFields = {
+ problem: [
+ 'description',
+ 'input_format',
+ 'output_format',
+ 'example',
+ 'limit_and_hint'
+ ],
+ contest: [
+ 'information',
+ 'problems'
+ ],
+ article: [
+ 'content'
+ ],
+ 'article-comment': [
+ 'content'
+ ]
+};
+
+const fn = async () => {
+ for (const model in modelFields) {
+ const modelObject = syzoj.model(model);
+ const allData = await modelObject.all();
+
+ let cnt = 0, tot = allData.length;
+ for (const obj of allData) {
+ console.log(`${model}: ${++cnt}/${tot}`);
+
+ let modified = false;
+
+ for (field of modelFields[model]) {
+ const processed = processMarkdown(obj[field]);
+ if (processed != obj[field]) {
+ obj[field] = processed;
+ modified = true;
+ }
+ }
+
+ if (modified) {
+ await obj.save();
+ }
+ }
+ }
+
+ process.exit();
+};
+
+// NOTE: Uncomment to run.
+// fn();