\p{Lu}\p{Ll}+(?:\p{Lu}\p{Ll}+|\d+)+)\b/u'; const RE_PAGE_LINK = '/(?:\[(?.+?)\])?\b(?<slug>\p{Lu}\p{Ll}+(?:\p{Lu}\p{Ll}+|\d+)+)(?:=(?<action>[a-z]+)\b)?/u'; const RE_HREF_LINK = '@(?:\[([^][]+)\])?([a-z]+://(\((?3)*\)|[^\s()<>]*[^\s().,;:?!<>{}*"\'])+)@ui'; const RE_FIGURE_IMAGE = '/^(?<slug>\p{Lu}\p{Ll}+(?:\p{Lu}\p{Ll}+|\d+)+)=image$/u'; const RE_FIGURE_LINK = '@^[a-z]+://[^\s]+$@ui'; const RE_WORD_BOUNDARY = '/((?<=\p{Ll}|\d)(?=\p{Lu})|(?<=\p{Ll})(?=\d))/u'; class State { public $pdo; public $revision_created = false; public $render_mode = 'html'; public $title; private $page_exists_stmt; public function __construct() { $this->pdo = new PDO('sqlite:./' . DB_NAME); $this->page_exists_stmt = $this->pdo->prepare(' SELECT 1 FROM revisions WHERE slug = ? LIMIT 1 ;'); if (isset($_SESSION['revision_created'])) { $this->revision_created = $_SESSION['revision_created']; unset($_SESSION['revision_created']); } } public function __invoke($buffer) { switch ($this->render_mode) { case 'html': return wrap_html($this->title, $buffer); case 'text': header('Content-Type: text/plain;'); return $buffer; default: return $buffer; } } public function PageExists($slug) { if ($slug === RECENT_CHANGES) { return true; } $this->page_exists_stmt->execute(array($slug)); return (bool)$this->page_exists_stmt->fetchColumn(); } } class Change { public $id; public $prev_id; public $slug; public $date_created; public $remote_ip; public $body; public $prev_body; private $time_created; private $remote_addr; public function __construct() { $this->date_created = DateTime::createFromFormat('U', $this->time_created); $this->remote_ip = inet_ntop($this->remote_addr); } public function DiffToHtml() { $diff = diff( preg_split('/(\s+)/', htmlspecialchars($this->prev_body ?: ''), -1, PREG_SPLIT_DELIM_CAPTURE), preg_split('/(\s+)/', htmlspecialchars($this->body ?: ''), -1, PREG_SPLIT_DELIM_CAPTURE)); $ret = ''; foreach ($diff as $k) { if (is_array($k)) { $ret .= (!empty($k['d'])?"<del>".implode($k['d'])."</del>":''). (!empty($k['i'])?"<ins>".implode($k['i'])."</ins>":''); } else { $ret .= $k; } } return $ret; } public function LineStats() { $diff = diff( preg_split('/\n/', $this->prev_body, -1, 0), preg_split('/\n/', $this->body, -1, 0)); $d = -0; $i = 0; foreach ($diff as $k) { if (is_array($k)) { $d += count(array_filter(array_map('trim', $k['d']))); $i += count(array_filter(array_map('trim', $k['i']))); } } return [-$d, $i]; } } class Revision { public $id; public $slug; public $title; public $body; public $date_created; public $image_hash; public $image_width; public $image_height; private $state; private $time_created; public function __construct($slug = null) { if ($this->time_created) { $this->date_created = DateTime::createFromFormat('U', $this->time_created); } if ($slug) { $this->slug = $slug; } $words = preg_split(RE_WORD_BOUNDARY, $this->slug); $this->title = implode(' ', $words); } public function Anchor($state) { $this->state = $state; } public function IntoHtml() { $inside_list = false; $inside_pre = false; foreach(explode(PHP_EOL, $this->body) as $line) { $line = htmlspecialchars($line); if (starts_with($line, '* ')) { if ($inside_pre) { $inside_pre = false; yield '</pre>'; } if ($inside_list == false) { $inside_list = true; yield '<ul>'; } $line = substr($line, 1); $line = $this->Inline($line); $line = $this->Linkify($line); yield '<li>' . $line . '</li>'; continue; } if (starts_with($line, ' ')) { if ($inside_list) { $inside_list = false; yield '</ul>'; } if ($inside_pre == false) { $inside_pre = true; yield '<pre>'; } $line = substr($line, 1); yield $line; continue; } if ($inside_list) { $inside_list = false; yield '</ul>'; } if ($inside_pre) { $inside_pre = false; yield '</pre>'; } if (starts_with($line, '---')) { yield '<hr>'; $heading = ltrim($line, '- '); if ($heading) { $heading = $this->Linkify($heading); yield '<h2>' . $heading . '</h2>'; } continue; } if (starts_with($line, '> ')) { $line = substr($line, 4); $line = $this->Inline($line); $line = $this->Linkify($line); yield '<blockquote>' . $line . '</blockquote>'; continue; } $line = trim($line); if (preg_match(RE_FIGURE_IMAGE, $line, $matches)) { $slug = $matches['slug']; yield "<figure><a href=\"?$slug\"><img src=\"?slug=$slug&action=image\" alt=\"$slug\" loading=lazy></a></figure>"; continue; } if (preg_match('#^https?://.+\.(jpg|jpeg|png|gif|webp)$#', $line)) { yield "<figure><img src=\"$line\" loading=lazy></figure>"; continue; } if (preg_match(RE_FIGURE_LINK, $line)) { yield "<figure><a href=\"$line\" target=_parent>$line</a></figure>"; continue; } if ($line != '') { $line = $this->Inline($line); $line = $this->Linkify($line); yield '<p>' . $line . '</p>'; continue; } } if ($inside_list) { yield '</ul>'; } if ($inside_pre) { yield '</pre>'; } } private function Linkify($text) { $parts = preg_split(RE_HREF_LINK, $text, -1, PREG_SPLIT_DELIM_CAPTURE); $result = ''; $part = current($parts); while ($part !== false) { $result .= $this->LinkifySlugs($part); $link_text = next($parts); $link_href = next($parts); if ($link_text) { $result .= "<a href=\"$link_href\" target=_parent>$link_text</a>"; } elseif ($link_href) { $result .= "<a href=\"$link_href\" target=_parent>$link_href</a>"; } // Pop the balanced parens capture group/subroutine. next($parts); $part = next($parts); } return $result; } private function LinkifySlugs($text) { $state = $this->state; return preg_replace_callback( RE_PAGE_LINK, function($matches) use(&$state) { $slug = $matches["slug"]; $missing = $state->PageExists($slug) ? '' : 'data-missing'; $href = "?$slug"; if ($action = $matches["action"]) { $href .= "=$action"; } if ($title = $matches["title"]) { return "<a $missing href=\"$href\">$title</a>"; } return "<a $missing href=\"$href\">$slug</a>"; }, $text, -1, $count, PREG_UNMATCHED_AS_NULL); } private function Inline($text) { return preg_replace( array('/\*([^ ](.*?[^ ])?)\*/', '/"(.+?)"/', '/`(.+?)`/'), array('<strong>$1</strong>', '<em>$1</em>', '<code>$1</code>'), $text); } } function starts_with($string, $prefix) { return substr($string, 0, strlen($prefix)) == $prefix; } session_cache_limiter('none'); session_start(); $state = new State(); switch (strtoupper($_SERVER['REQUEST_METHOD'])) { case 'GET': if (empty($_GET)) { header('Location: ?' . MAIN_PAGE, true, 303); exit; } $slug = filter_input(INPUT_GET, 'slug'); if (empty($slug)) { if (USE_MULTICOLUMN) { render_viewer(); exit; } else { $slug = array_key_first($_GET); $action = $_GET[$slug]; if (is_array($action)) { $id = array_key_first($action); $action = $action[$id]; $state->title = $slug . ($action ? "[$id]=$action" : "[$id]"); } else { $id = null; $state->title = $slug . ($action ? "=$action" : ''); } } } else { $state->title = $slug; $id = filter_input(INPUT_GET, 'id'); $action = filter_input(INPUT_GET, 'action'); } ob_start($state); if (!filter_var($slug, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => RE_PAGE_SLUG]])) { render_invalid_slug($slug); exit; } if ($id && !filter_var($id, FILTER_VALIDATE_INT)) { render_invalid_revision($slug, $id); exit; } switch ($slug) { case RECENT_CHANGES: $remote_ip = $action; if ($remote_ip == null) { view_recent_changes($state, $id); } elseif (filter_var($remote_ip, FILTER_VALIDATE_IP)) { view_recent_changes_from($state, $remote_ip, $id); } else { render_invalid_address($slug, $remote_ip); } return; } switch ($action) { case 'diff': view_diff_at_revision($state, $slug, $id); break; case 'edit': view_edit($state, $slug, $id); break; case 'history': view_history($state, $slug, $id); break; case 'backlinks': view_backlinks($state, $slug, $id); break; case 'text': $state->render_mode = 'text'; case 'html': case '': if ($id) { view_page_at_revision($state, $slug, $id); } else { view_page_latest($state, $slug); } break; case 'image': if ($id) { view_image_at_revision($state, $slug, $id); } else { view_image_latest($state, $slug); } break; default: render_invalid_action($slug, $action); } ob_end_flush(); break; case 'POST': $honeypot = filter_input(INPUT_POST, 'user'); if ($honeypot) { header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed', true, 405); exit(); } $body = filter_input(INPUT_POST, 'body'); $time = date('U'); $addr = inet_pton($_SERVER['REMOTE_ADDR']); $slug = filter_input(INPUT_POST, 'slug'); $image_hash = filter_input(INPUT_POST, 'image_hash'); if (!filter_var($slug, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => RE_PAGE_SLUG]])) { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); exit('Invalid page slug; must be CamelCase123.'); } $state->pdo->beginTransaction(); $image_file = $_FILES['image_data']; if (file_exists($image_file['tmp_name']) && is_uploaded_file($image_file['tmp_name'])) { if ($image_file['error'] > UPLOAD_ERR_OK) { // File is larger than php.ini's upload_max_filesize. header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); exit('File too big; max allowed is ' . floor(IMAGE_MAX_BYTES / 1024) . 'kb.'); } $image_file_type = mime_content_type($image_file['tmp_name']); if (extension_loaded('gd')) { switch ($image_file_type) { case 'image/jpeg': $image = imagecreatefromjpeg($image_file['tmp_name']); break; case 'image/png': $image = imagecreatefrompng($image_file['tmp_name']); imagepalettetotruecolor($image); imagealphablending($image, true); imagesavealpha($image, true); break; case 'image/webp': $image = imagecreatefromwebp($image_file['tmp_name']); break; default: header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); exit('Only JPG, PNG, WEBP are accepted.'); } $image_width = imagesx($image); if ($image_width > IMAGE_MAX_WIDTH) { $image = imagescale($image, IMAGE_MAX_WIDTH); $image_width = imagesx($image); } $image_height = imagesy($image); if ($image_file_type != 'image/webp') { $image_file_type = 'image/webp'; $image_temp_name = $image_file['tmp_name'] . '.webp'; imagewebp($image, $image_temp_name, 84); } else { $image_temp_name = $image_file['tmp_name']; } $image_file_size = filesize($image_temp_name); } elseif (starts_with($image_file_type, 'image/')) { $image_temp_name = $image_file['tmp_name']; $image_file_size = $image_file['size']; } else { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); exit('File is not an image; MIME type must be image/*.'); } if ($image_file_size > IMAGE_MAX_BYTES) { // File is larger than our custom limit. header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); exit('File too big; max allowed is ' . floor(IMAGE_MAX_BYTES / 1024) . 'kb.'); } $statement = $state->pdo->prepare(' INSERT OR IGNORE INTO images (hash, page_slug, content_type, time_created, remote_addr, image_data, image_width, image_height, file_size, file_name) VALUES (:hash, :page_slug, :content_type, :time_created, :remote_addr, :image_data, :image_width, :image_height, :file_size, :file_name) ;'); $image_hash = sha1_file($image_temp_name); $file = fopen($image_temp_name, 'rb'); $statement->bindParam('hash', $image_hash, PDO::PARAM_STR); $statement->bindParam('page_slug', $slug, PDO::PARAM_STR); $statement->bindParam('content_type', $image_file_type, PDO::PARAM_STR); $statement->bindParam('time_created', $time, PDO::PARAM_INT); $statement->bindParam('remote_addr', $addr, PDO::PARAM_STR); $statement->bindParam('image_data', $file, PDO::PARAM_LOB); $statement->bindParam('image_width', $image_width, PDO::PARAM_INT); $statement->bindParam('image_height', $image_height, PDO::PARAM_INT); $statement->bindParam('file_size', $image_file_size, PDO::PARAM_INT); $statement->bindParam('file_name', $image_file['name'], PDO::PARAM_STR); if (!$statement->execute()) { exit('Unable to upload the image.'); } } elseif (empty($image_hash)) { $image_hash = null; } elseif (!filter_var($image_hash, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => RE_HASH_SHA1]])) { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); exit('Invalid image_hash; must be SHA1 or empty.'); } $statement = $state->pdo->prepare(' INSERT INTO revisions (slug, body, time_created, remote_addr, image_hash) VALUES (:slug, :body, :time, :addr, :image_hash) ;'); $statement->bindParam('slug', $slug, PDO::PARAM_STR); $statement->bindParam('body', $body, PDO::PARAM_STR); $statement->bindParam('time', $time, PDO::PARAM_INT); $statement->bindParam('addr', $addr, PDO::PARAM_STR); $statement->bindParam('image_hash', $image_hash, PDO::PARAM_STR); if ($statement->execute()) { $state->pdo->commit(); $_SESSION['revision_created'] = true; if (USE_MULTICOLUMN) { header("Location: ?slug=$slug", true, 303); } else { header("Location: ?$slug", true, 303); } exit; } exit("Unable to create a new revision of $slug."); } // Views. function view_page_latest($state, $slug) { $statement = $state->pdo->prepare(' SELECT id, slug, body, revisions.time_created, image_hash, image_width, image_height FROM revisions LEFT JOIN images ON image_hash = hash WHERE slug = ? ORDER BY id DESC LIMIT 1 ;'); $statement->execute(array($slug)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Revision'); $page = $statement->fetch(); if (!$page) { render_page_not_found($slug); return; } $page->Anchor($state); if ($state->render_mode === 'text') { render_source($page); } else { render_latest($page, $state); } } function view_page_at_revision($state, $slug, $id) { $statement = $state->pdo->prepare(' SELECT id, slug, body, revisions.time_created, image_hash, image_width, image_height FROM revisions LEFT JOIN images ON image_hash = hash WHERE slug = ? AND id = ? ;'); $statement->execute(array($slug, $id)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Revision'); $page = $statement->fetch(); if (!$page) { render_revision_not_found($slug, $id); return; } $page->Anchor($state); if ($state->render_mode === 'text') { render_source($page); } else { render_revision($page); } } function view_image_latest($state, $slug) { $statement = $state->pdo->prepare(' SELECT hash, content_type, image_data, file_size FROM images JOIN revisions ON hash == image_hash WHERE revisions.slug = ? ORDER BY id DESC LIMIT 1 ;'); $statement->execute(array($slug)); $statement->bindColumn('hash', $image_hash, PDO::PARAM_STR); $statement->bindColumn('content_type', $content_type, PDO::PARAM_STR); $statement->bindColumn('image_data', $image_data, PDO::PARAM_LOB); $statement->bindColumn('file_size', $file_size, PDO::PARAM_INT); if ($statement->fetch(PDO::FETCH_BOUND)) { $state->render_mode = $content_type; header("Content-Type: $content_type"); header("Content-Length: $file_size"); header("ETag: $image_hash"); header('Cache-Control: max-age=' . CACHE_MAX_AGE); if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $image_hash) { header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); ob_end_flush(); } else { ob_end_flush(); fpassthru($image_data); } } elseif ($state->PageExists($slug)) { render_image_not_found($slug); } else { render_page_not_found($slug); } } function view_image_at_revision($state, $slug, $id) { $statement = $state->pdo->prepare(' SELECT hash, content_type, image_data, file_size FROM images JOIN revisions ON hash == image_hash WHERE revisions.slug = ? AND revisions.id = ? ;'); $statement->execute(array($slug, $id)); $statement->bindColumn('hash', $image_hash, PDO::PARAM_STR); $statement->bindColumn('content_type', $content_type, PDO::PARAM_STR); $statement->bindColumn('image_data', $image_data, PDO::PARAM_LOB); $statement->bindColumn('file_size', $file_size, PDO::PARAM_INT); if ($statement->fetch(PDO::FETCH_BOUND)) { $state->render_mode = $content_type; header("Content-Type: $content_type"); header("Content-Length: $file_size"); header("ETag: $image_hash"); header('Cache-Control: max-age=' . CACHE_MAX_AGE); if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $image_hash) { header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified'); ob_end_flush(); } else { ob_end_flush(); fpassthru($image_data); } } elseif ($state->PageExists($slug)) { render_image_not_found($slug, $id); } else { render_revision_not_found($slug, $id); } } function view_edit($state, $slug, $id) { $statement = $state->pdo->prepare($id ? ' SELECT id, slug, body, revisions.time_created, image_hash, image_width, image_height FROM revisions LEFT JOIN images ON image_hash = hash WHERE slug = ? AND id = ? ;' : ' SELECT id, slug, body, revisions.time_created, image_hash, image_width, image_height FROM revisions LEFT JOIN images ON image_hash = hash WHERE slug = ? ORDER BY id DESC LIMIT 1 ;'); $statement->execute($id ? array($slug, $id) : array($slug)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Revision'); $page = $statement->fetch(); if (!$page) { if ($id !== null) { render_revision_not_found($slug, $id); return; } $page = new Revision($slug); } render_edit($page); } function view_history($state, $slug, $id) { $statement = $state->pdo->prepare(' SELECT slug, id, time_created, remote_addr, body, LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id, LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body FROM revisions WHERE slug = ?' . ($id ? ' AND id <= ?' : '') . ' ORDER BY id DESC ;'); $statement->execute($id ? array($slug, $id) : array($slug)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Change'); $changes = $statement->fetchAll(); if (!$changes) { render_page_not_found($slug); } else { render_history($slug, $changes); } } function view_diff_at_revision($state, $slug, $id) { $statement = $state->pdo->prepare(' SELECT slug, id, time_created, remote_addr, body, LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id, LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body FROM revisions WHERE slug = ?' . ($id ? ' AND id <= ?' : '') . ' ORDER BY id DESC LIMIT 1 ;'); $statement->execute($id ? array($slug, $id) : array($slug)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Change'); $change = $statement->fetch(); if (!$change) { render_page_not_found($slug); } else { render_diff($change); } } function view_backlinks($state, $slug, $id) { $statement = $state->pdo->prepare(' SELECT DISTINCT slug FROM revisions WHERE slug != ? AND body LIKE ?' . ($id ? ' AND id <= ?' : '') . ' ;'); $statement->execute($id ? array($slug, "%$slug%", $id) : array($slug, "%$slug%")); $references = $statement->fetchAll(PDO::FETCH_OBJ); render_backlinks($slug, $references); } function view_recent_changes($state, $p = 0) { if ($p > 0) { render_invalid_offset(RECENT_CHANGES, $p); return; } $limit = 25; $statement = $state->pdo->prepare(' SELECT slug, id, time_created, remote_addr, body, LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id, LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body FROM revisions ORDER BY id DESC LIMIT ? OFFSET ? ;'); $statement->execute(array($limit, $limit * $p * -1)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Change'); $changes = $statement->fetchAll(); render_recent_changes($p, $changes); } function view_recent_changes_from($state, $remote_ip, $p = 0) { if ($p > 0) { render_invalid_offset(RECENT_CHANGES, $p); return; } $limit = 25; $statement = $state->pdo->prepare(' WITH recent_changes AS ( SELECT slug, id, time_created, remote_addr, body, LEAD(id, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_id, LEAD(body, 1, 0) OVER (PARTITION BY slug ORDER BY id DESC) prev_body FROM revisions ) SELECT * FROM recent_changes WHERE remote_addr = ? ORDER BY id DESC LIMIT ? OFFSET ? ;'); $statement->execute(array(inet_pton($remote_ip), $limit, $limit * $p * -1)); $statement->setFetchMode(PDO::FETCH_CLASS, 'Change'); $changes = $statement->fetchAll(); render_recent_changes_from($remote_ip, $p, $changes); } // Rendering templates. function render_viewer() { ?> <!doctype html> <title><?=MAIN_PAGE?> $title $buffer EOF; } function render_invalid_slug($slug) { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); ?>

Invalid Page Name

is not a valid page name.


Invalid Revision

[] is not a valid revision.


Invalid Action

is not a valid action name.


Invalid Range

[] is not a valid range offset.


Invalid Address

is not a valid IP address.


Page Not Found

doesn't exist yet. Create?


Revision Not Found

[] doesn't exist.


Image Not Found

[] doesn't have an image.

Image Not Found

doesn't have an image. Edit?


title [$page->id] {$page->date_created->format(AS_DATE)} $page->body "; } function render_latest($page, $state) { if (USE_MULTICOLUMN && $page->image_hash) { $img_width = min($page->image_width, 470); $img_height = min($page->image_height, floor(470 * $page->image_height / $page->image_width)); } else { $img_width = $page->image_width; $img_height = $page->image_height; } ?>
revision_created): ?>
Page updated successfully.

title?>

image_hash): ?>
IntoHtml() as $elem): ?>
image_hash) { $img_width = min($page->image_width, 470); $img_height = min($page->image_height, floor(470 * $page->image_height / $page->image_width)); } else { $img_width = $page->image_width; $img_height = $page->image_height; } ?>

title?> [id?>]

image_hash): ?>
IntoHtml() as $elem): ?>
image_hash) { $img_width = min($page->image_width, 470); $img_height = min($page->image_height, floor(470 * $page->image_height / $page->image_width)); } else { $img_width = $page->image_width; $img_height = $page->image_height; } ?>

Edit title?> [id?>]

image_hash): ?>

Revision history for


Diff for slug?> [prev_id?>] [id?>]

DiffToHtml()?>

What links to ?


Recent Changes

next


Recent Changes from

next


// May be used and distributed under the zlib/libpng license. // https://paulbutler.org/2007/a-simple-diff-algorithm-in-php/ function diff($old, $new){ $matrix = array(); $maxlen = 1; foreach ($old as $oindex => $ovalue) { $nkeys = array_keys($new, $ovalue); foreach ($nkeys as $nindex) { $matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ? $matrix[$oindex - 1][$nindex - 1] + 1 : 1; if ($matrix[$oindex][$nindex] > $maxlen) { $maxlen = $matrix[$oindex][$nindex]; $omax = $oindex + 1 - $maxlen; $nmax = $nindex + 1 - $maxlen; } } } if ($maxlen == 1) { return array(array('d'=>$old, 'i'=>$new)); } return array_merge( diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)), array_slice($new, $nmax, $maxlen), diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen))); }