aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Zotlabs/Module/Wiki.php43
-rw-r--r--include/wiki.php44
-rw-r--r--library/class.Diff.php386
-rw-r--r--view/tpl/wiki.tpl48
-rw-r--r--view/tpl/wiki_page_history.tpl46
5 files changed, 550 insertions, 17 deletions
diff --git a/Zotlabs/Module/Wiki.php b/Zotlabs/Module/Wiki.php
index 9e7d151b5..bef831de8 100644
--- a/Zotlabs/Module/Wiki.php
+++ b/Zotlabs/Module/Wiki.php
@@ -136,6 +136,16 @@ class Wiki extends \Zotlabs\Web\Controller {
// Render the Markdown-formatted page content in HTML
require_once('library/markdown.php');
+ $wikiModalID = random_string(3);
+ $wikiModal = replace_macros(
+ get_markup_template('generic_modal.tpl'), array(
+ '$id' => $wikiModalID,
+ '$title' => t('Revision Comparison'),
+ '$ok' => t('Revert'),
+ '$cancel' => t('Cancel')
+ )
+ );
+
$o .= replace_macros(get_markup_template('wiki.tpl'),array(
'$wikiheaderName' => $wikiheaderName,
'$wikiheaderPage' => $wikiheaderPage,
@@ -157,7 +167,10 @@ class Wiki extends \Zotlabs\Web\Controller {
'$pageName' => array('pageName', t('Enter the name of the new page:'), '', ''),
'$pageRename' => array('pageRename', t('Enter the new name:'), '', ''),
'$commitMsg' => array('commitMsg', '', '', '', '', 'placeholder="(optional) Enter a custom message when saving the page..."'),
- '$pageHistory' => $pageHistory['history']
+ '$pageHistory' => $pageHistory['history'],
+ '$wikiModal' => $wikiModal,
+ '$wikiModalID' => $wikiModalID,
+ '$commit' => 'HEAD'
));
head_add_js('library/ace/ace.js'); // Ace Code Editor
return $o;
@@ -412,7 +425,7 @@ class Wiki extends \Zotlabs\Web\Controller {
json_return_and_die(array('success' => false));
}
}
- $reverted = wiki_revert_page(array('commitHash' => $commitHash, 'observer' => \App::get_observer(), 'resource_id' => $resource_id, 'pageUrlName' => $pageUrlName));
+ $reverted = wiki_revert_page(array('commitHash' => $commitHash, 'resource_id' => $resource_id, 'pageUrlName' => $pageUrlName));
if($reverted['success']) {
json_return_and_die(array('content' => $reverted['content'], 'message' => '', 'success' => true));
} else {
@@ -420,6 +433,32 @@ class Wiki extends \Zotlabs\Web\Controller {
}
}
+ // Compare page revisions
+ if ((argc() === 4) && (argv(2) === 'compare') && (argv(3) === 'page')) {
+ $resource_id = $_POST['resource_id'];
+ $pageUrlName = $_POST['name'];
+ $compareCommit = $_POST['compareCommit'];
+ $currentCommit = $_POST['currentCommit'];
+ // Determine if observer has permission to revert pages
+ $nick = argv(1);
+ $channel = get_channel_by_nick($nick);
+ if (local_channel() !== intval($channel['channel_id'])) {
+ $observer_hash = get_observer_hash();
+ $perms = wiki_get_permissions($resource_id, intval($channel['channel_id']), $observer_hash);
+ if(!$perms['read']) {
+ logger('Wiki read permission denied.' . EOL);
+ json_return_and_die(array('success' => false));
+ }
+ }
+ $compare = wiki_compare_page(array('currentCommit' => $currentCommit, 'compareCommit' => $compareCommit, 'resource_id' => $resource_id, 'pageUrlName' => $pageUrlName));
+ if($compare['success']) {
+ $diffHTML = '<table class="text-center" width="100%"><tr><td class="lead" width="50%">Current Revision</td><td class="lead" width="50%">Selected Revision</td></tr></table>' . $compare['diff'];
+ json_return_and_die(array('diff' => $diffHTML, 'message' => '', 'success' => true));
+ } else {
+ json_return_and_die(array('diff' => '', 'message' => 'Error comparing page', 'success' => false));
+ }
+ }
+
// Rename a page
if ((argc() === 4) && (argv(2) === 'rename') && (argv(3) === 'page')) {
$resource_id = $_POST['resource_id'];
diff --git a/include/wiki.php b/include/wiki.php
index d60f4a3a7..a89db3358 100644
--- a/include/wiki.php
+++ b/include/wiki.php
@@ -347,7 +347,7 @@ function wiki_revert_page($arr) {
$resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : '');
$commitHash = ((array_key_exists('commitHash',$arr)) ? $arr['commitHash'] : null);
if (! $commitHash) {
- return array('content' => $content, 'message' => 'No commit has provided', 'success' => false);
+ return array('content' => $content, 'message' => 'No commit was provided', 'success' => false);
}
$w = wiki_get_wiki($resource_id);
if (!$w['path']) {
@@ -378,6 +378,48 @@ function wiki_revert_page($arr) {
}
}
+function wiki_compare_page($arr) {
+ $pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : '');
+ $resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : '');
+ $currentCommit = ((array_key_exists('currentCommit',$arr)) ? $arr['currentCommit'] : 'HEAD');
+ $compareCommit = ((array_key_exists('compareCommit',$arr)) ? $arr['compareCommit'] : null);
+ if (! $compareCommit) {
+ return array('message' => 'No compare commit was provided', 'success' => false);
+ }
+ $w = wiki_get_wiki($resource_id);
+ if (!$w['path']) {
+ return array('message' => 'Error reading wiki', 'success' => false);
+ }
+ $page_path = $w['path'].'/'.$pageUrlName.'.md';
+ if (is_readable($page_path) === true) {
+ $reponame = ((array_key_exists('title', $w['wiki'])) ? urlencode($w['wiki']['title']) : 'repo');
+ if($reponame === '') {
+ $reponame = 'repo';
+ }
+ $git = new GitRepo('', null, false, $w['wiki']['title'], $w['path']);
+ $compareContent = $currentContent = '';
+ try {
+ foreach ($git->git->tree($currentCommit) as $object) {
+ if ($object['type'] == 'blob' && $object['file'] === $pageUrlName.'.md' ) {
+ $currentContent = $git->git->cat->blob($object['hash']);
+ }
+ }
+ foreach ($git->git->tree($compareCommit) as $object) {
+ if ($object['type'] == 'blob' && $object['file'] === $pageUrlName.'.md' ) {
+ $compareContent = $git->git->cat->blob($object['hash']);
+ }
+ }
+ require_once('library/class.Diff.php');
+ $diff = Diff::toTable(Diff::compare($currentContent, $compareContent));
+ } catch (\PHPGit\Exception\GitException $e) {
+ return array('message' => 'GitRepo error thrown', 'success' => false);
+ }
+ return array('diff' => $diff, 'message' => '', 'success' => true);
+ } else {
+ return array('message' => 'Page file not writable', 'success' => false);
+ }
+}
+
function wiki_git_commit($arr) {
$files = ((array_key_exists('files', $arr)) ? $arr['files'] : null);
$all = ((array_key_exists('all', $arr)) ? $arr['all'] : false);
diff --git a/library/class.Diff.php b/library/class.Diff.php
new file mode 100644
index 000000000..689abe9e7
--- /dev/null
+++ b/library/class.Diff.php
@@ -0,0 +1,386 @@
+<?php
+
+/*
+
+class.Diff.php
+
+A class containing a diff implementation
+
+Created by Stephen Morley - http://stephenmorley.org/ - and released under the
+terms of the CC0 1.0 Universal legal code:
+
+http://creativecommons.org/publicdomain/zero/1.0/legalcode
+
+*/
+
+// A class containing functions for computing diffs and formatting the output.
+class Diff{
+
+ // define the constants
+ const UNMODIFIED = 0;
+ const DELETED = 1;
+ const INSERTED = 2;
+
+ /* Returns the diff for two strings. The return value is an array, each of
+ * whose values is an array containing two values: a line (or character, if
+ * $compareCharacters is true), and one of the constants DIFF::UNMODIFIED (the
+ * line or character is in both strings), DIFF::DELETED (the line or character
+ * is only in the first string), and DIFF::INSERTED (the line or character is
+ * only in the second string). The parameters are:
+ *
+ * $string1 - the first string
+ * $string2 - the second string
+ * $compareCharacters - true to compare characters, and false to compare
+ * lines; this optional parameter defaults to false
+ */
+ public static function compare(
+ $string1, $string2, $compareCharacters = false){
+
+ // initialise the sequences and comparison start and end positions
+ $start = 0;
+ if ($compareCharacters){
+ $sequence1 = $string1;
+ $sequence2 = $string2;
+ $end1 = strlen($string1) - 1;
+ $end2 = strlen($string2) - 1;
+ }else{
+ $sequence1 = preg_split('/\R/', $string1);
+ $sequence2 = preg_split('/\R/', $string2);
+ $end1 = count($sequence1) - 1;
+ $end2 = count($sequence2) - 1;
+ }
+
+ // skip any common prefix
+ while ($start <= $end1 && $start <= $end2
+ && $sequence1[$start] == $sequence2[$start]){
+ $start ++;
+ }
+
+ // skip any common suffix
+ while ($end1 >= $start && $end2 >= $start
+ && $sequence1[$end1] == $sequence2[$end2]){
+ $end1 --;
+ $end2 --;
+ }
+
+ // compute the table of longest common subsequence lengths
+ $table = self::computeTable($sequence1, $sequence2, $start, $end1, $end2);
+
+ // generate the partial diff
+ $partialDiff =
+ self::generatePartialDiff($table, $sequence1, $sequence2, $start);
+
+ // generate the full diff
+ $diff = array();
+ for ($index = 0; $index < $start; $index ++){
+ $diff[] = array($sequence1[$index], self::UNMODIFIED);
+ }
+ while (count($partialDiff) > 0) $diff[] = array_pop($partialDiff);
+ for ($index = $end1 + 1;
+ $index < ($compareCharacters ? strlen($sequence1) : count($sequence1));
+ $index ++){
+ $diff[] = array($sequence1[$index], self::UNMODIFIED);
+ }
+
+ // return the diff
+ return $diff;
+
+ }
+
+ /* Returns the diff for two files. The parameters are:
+ *
+ * $file1 - the path to the first file
+ * $file2 - the path to the second file
+ * $compareCharacters - true to compare characters, and false to compare
+ * lines; this optional parameter defaults to false
+ */
+ public static function compareFiles(
+ $file1, $file2, $compareCharacters = false){
+
+ // return the diff of the files
+ return self::compare(
+ file_get_contents($file1),
+ file_get_contents($file2),
+ $compareCharacters);
+
+ }
+
+ /* Returns the table of longest common subsequence lengths for the specified
+ * sequences. The parameters are:
+ *
+ * $sequence1 - the first sequence
+ * $sequence2 - the second sequence
+ * $start - the starting index
+ * $end1 - the ending index for the first sequence
+ * $end2 - the ending index for the second sequence
+ */
+ private static function computeTable(
+ $sequence1, $sequence2, $start, $end1, $end2){
+
+ // determine the lengths to be compared
+ $length1 = $end1 - $start + 1;
+ $length2 = $end2 - $start + 1;
+
+ // initialise the table
+ $table = array(array_fill(0, $length2 + 1, 0));
+
+ // loop over the rows
+ for ($index1 = 1; $index1 <= $length1; $index1 ++){
+
+ // create the new row
+ $table[$index1] = array(0);
+
+ // loop over the columns
+ for ($index2 = 1; $index2 <= $length2; $index2 ++){
+
+ // store the longest common subsequence length
+ if ($sequence1[$index1 + $start - 1]
+ == $sequence2[$index2 + $start - 1]){
+ $table[$index1][$index2] = $table[$index1 - 1][$index2 - 1] + 1;
+ }else{
+ $table[$index1][$index2] =
+ max($table[$index1 - 1][$index2], $table[$index1][$index2 - 1]);
+ }
+
+ }
+ }
+
+ // return the table
+ return $table;
+
+ }
+
+ /* Returns the partial diff for the specificed sequences, in reverse order.
+ * The parameters are:
+ *
+ * $table - the table returned by the computeTable function
+ * $sequence1 - the first sequence
+ * $sequence2 - the second sequence
+ * $start - the starting index
+ */
+ private static function generatePartialDiff(
+ $table, $sequence1, $sequence2, $start){
+
+ // initialise the diff
+ $diff = array();
+
+ // initialise the indices
+ $index1 = count($table) - 1;
+ $index2 = count($table[0]) - 1;
+
+ // loop until there are no items remaining in either sequence
+ while ($index1 > 0 || $index2 > 0){
+
+ // check what has happened to the items at these indices
+ if ($index1 > 0 && $index2 > 0
+ && $sequence1[$index1 + $start - 1]
+ == $sequence2[$index2 + $start - 1]){
+
+ // update the diff and the indices
+ $diff[] = array($sequence1[$index1 + $start - 1], self::UNMODIFIED);
+ $index1 --;
+ $index2 --;
+
+ }elseif ($index2 > 0
+ && $table[$index1][$index2] == $table[$index1][$index2 - 1]){
+
+ // update the diff and the indices
+ $diff[] = array($sequence2[$index2 + $start - 1], self::INSERTED);
+ $index2 --;
+
+ }else{
+
+ // update the diff and the indices
+ $diff[] = array($sequence1[$index1 + $start - 1], self::DELETED);
+ $index1 --;
+
+ }
+
+ }
+
+ // return the diff
+ return $diff;
+
+ }
+
+ /* Returns a diff as a string, where unmodified lines are prefixed by ' ',
+ * deletions are prefixed by '- ', and insertions are prefixed by '+ '. The
+ * parameters are:
+ *
+ * $diff - the diff array
+ * $separator - the separator between lines; this optional parameter defaults
+ * to "\n"
+ */
+ public static function toString($diff, $separator = "\n"){
+
+ // initialise the string
+ $string = '';
+
+ // loop over the lines in the diff
+ foreach ($diff as $line){
+
+ // extend the string with the line
+ switch ($line[1]){
+ case self::UNMODIFIED : $string .= ' ' . $line[0];break;
+ case self::DELETED : $string .= '- ' . $line[0];break;
+ case self::INSERTED : $string .= '+ ' . $line[0];break;
+ }
+
+ // extend the string with the separator
+ $string .= $separator;
+
+ }
+
+ // return the string
+ return $string;
+
+ }
+
+ /* Returns a diff as an HTML string, where unmodified lines are contained
+ * within 'span' elements, deletions are contained within 'del' elements, and
+ * insertions are contained within 'ins' elements. The parameters are:
+ *
+ * $diff - the diff array
+ * $separator - the separator between lines; this optional parameter defaults
+ * to '<br>'
+ */
+ public static function toHTML($diff, $separator = '<br>'){
+
+ // initialise the HTML
+ $html = '';
+
+ // loop over the lines in the diff
+ foreach ($diff as $line){
+
+ // extend the HTML with the line
+ switch ($line[1]){
+ case self::UNMODIFIED : $element = 'span'; break;
+ case self::DELETED : $element = 'del'; break;
+ case self::INSERTED : $element = 'ins'; break;
+ }
+ $html .=
+ '<' . $element . '>'
+ . htmlspecialchars($line[0])
+ . '</' . $element . '>';
+
+ // extend the HTML with the separator
+ $html .= $separator;
+
+ }
+
+ // return the HTML
+ return $html;
+
+ }
+
+ /* Returns a diff as an HTML table. The parameters are:
+ *
+ * $diff - the diff array
+ * $indentation - indentation to add to every line of the generated HTML; this
+ * optional parameter defaults to ''
+ * $separator - the separator between lines; this optional parameter
+ * defaults to '<br>'
+ */
+ public static function toTable($diff, $indentation = '', $separator = '<br>'){
+
+ // initialise the HTML
+ $html = $indentation . "<table class=\"diff\">\n";
+
+ // loop over the lines in the diff
+ $index = 0;
+ while ($index < count($diff)){
+
+ // determine the line type
+ switch ($diff[$index][1]){
+
+ // display the content on the left and right
+ case self::UNMODIFIED:
+ $leftCell =
+ self::getCellContent(
+ $diff, $indentation, $separator, $index, self::UNMODIFIED);
+ $rightCell = $leftCell;
+ break;
+
+ // display the deleted on the left and inserted content on the right
+ case self::DELETED:
+ $leftCell =
+ self::getCellContent(
+ $diff, $indentation, $separator, $index, self::DELETED);
+ $rightCell =
+ self::getCellContent(
+ $diff, $indentation, $separator, $index, self::INSERTED);
+ break;
+
+ // display the inserted content on the right
+ case self::INSERTED:
+ $leftCell = '';
+ $rightCell =
+ self::getCellContent(
+ $diff, $indentation, $separator, $index, self::INSERTED);
+ break;
+
+ }
+
+ // extend the HTML with the new row
+ $html .=
+ $indentation
+ . " <tr>\n"
+ . $indentation
+ . ' <td class="diff'
+ . ($leftCell == $rightCell
+ ? 'Unmodified'
+ : ($leftCell == '' ? 'Blank' : 'Deleted'))
+ . '">'
+ . $leftCell
+ . "</td>\n"
+ . $indentation
+ . ' <td class="diff'
+ . ($leftCell == $rightCell
+ ? 'Unmodified'
+ : ($rightCell == '' ? 'Blank' : 'Inserted'))
+ . '">'
+ . $rightCell
+ . "</td>\n"
+ . $indentation
+ . " </tr>\n";
+
+ }
+
+ // return the HTML
+ return $html . $indentation . "</table>\n";
+
+ }
+
+ /* Returns the content of the cell, for use in the toTable function. The
+ * parameters are:
+ *
+ * $diff - the diff array
+ * $indentation - indentation to add to every line of the generated HTML
+ * $separator - the separator between lines
+ * $index - the current index, passes by reference
+ * $type - the type of line
+ */
+ private static function getCellContent(
+ $diff, $indentation, $separator, &$index, $type){
+
+ // initialise the HTML
+ $html = '';
+
+ // loop over the matching lines, adding them to the HTML
+ while ($index < count($diff) && $diff[$index][1] == $type){
+ $html .=
+ '<span>'
+ . htmlspecialchars($diff[$index][0])
+ . '</span>'
+ . $separator;
+ $index ++;
+ }
+
+ // return the HTML
+ return $html;
+
+ }
+
+}
+
+?>
diff --git a/view/tpl/wiki.tpl b/view/tpl/wiki.tpl
index dc78aad9f..aa0b88545 100644
--- a/view/tpl/wiki.tpl
+++ b/view/tpl/wiki.tpl
@@ -96,17 +96,6 @@
</div>
<div id="page-history-pane" class="tab-pane fade" {{if $hidePageHistory}}style="display: none;"{{/if}}>
<div id="page-history-list" class="section-content-wrapper">
- <table class="table-striped table-responsive table-hover" style="width: 100%;">
- {{foreach $pageHistory as $commit}}
- <tr><td>
- <table>
- <tr><td>Date</td><td>{{$commit.date}}</td></tr>
- <tr><td>Name</td><td>{{$commit.name}}</td></tr>
- <tr><td>Message</td><td>{{$commit.title}}</td></tr>
- </table>
- </td></tr>
- {{/foreach}}
- </table>
</div>
</div>
@@ -120,16 +109,22 @@
{{/if}}
</div>
+{{$wikiModal}}
+
<script>
window.wiki_resource_id = '{{$resource_id}}';
window.wiki_page_name = '{{$page}}';
window.wiki_page_content = {{$content}};
+ window.wiki_page_commit = '{{$commit}}';
if (window.wiki_page_name === 'Home') {
$('#delete-page').hide();
$('#rename-page').hide();
}
+ $("#generic-modal-ok-{{$wikiModalID}}").removeClass('btn-primary');
+ $("#generic-modal-ok-{{$wikiModalID}}").addClass('btn-danger');
+
$('#rename-page').click(function (ev) {
$('#rename-page-form-wrapper').show();
});
@@ -261,6 +256,7 @@ function wiki_delete_wiki(wikiHtmlName, resource_id) {
window.console.log('Page saved successfully.');
window.wiki_page_content = currentContent;
$('#id_commitMsg').val(''); // Clear the commit message box
+ $('#wiki-get-history').click();
} else {
alert('Error saving page.'); // TODO: Replace alerts with auto-timeout popups
window.console.log('Error saving page.');
@@ -306,6 +302,7 @@ function wiki_delete_wiki(wikiHtmlName, resource_id) {
$('#revert-'+commitHash).removeClass('btn-danger');
$('#revert-'+commitHash).addClass('btn-success');
$('#revert-'+commitHash).html('Page reverted<br>but not saved');
+ window.wiki_page_commit = commitHash;
// put contents in editor
editor.getSession().setValue(data.content);
} else {
@@ -313,4 +310,33 @@ function wiki_delete_wiki(wikiHtmlName, resource_id) {
}
}, 'json');
}
+
+ function wiki_compare_page(compareCommit) {
+ if (window.wiki_resource_id === '' || window.wiki_page_name === '' || window.wiki_page_commit === '') {
+ window.console.log('You must have a wiki page open in order to revert pages.');
+ return false;
+ }
+ $.post("wiki/{{$channel}}/compare/page",
+ {
+ compareCommit: compareCommit,
+ currentCommit: window.wiki_page_commit,
+ name: window.wiki_page_name,
+ resource_id: window.wiki_resource_id
+ },
+ function (data) {
+ if (data.success) {
+ var modalBody = $('#generic-modal-body-{{$wikiModalID}}');
+ modalBody.html('<div class="descriptive-text">'+data.diff+'</div>');
+ $('.modal-dialog').width('80%');
+ $("#generic-modal-ok-{{$wikiModalID}}").off('click');
+ $("#generic-modal-ok-{{$wikiModalID}}").click(function () {
+ wiki_revert_page(compareCommit);
+ $('#generic-modal-{{$wikiModalID}}').modal('hide');
+ });
+ $('#generic-modal-{{$wikiModalID}}').modal();
+ } else {
+ window.console.log('Error comparing page.');
+ }
+ }, 'json');
+ }
</script>
diff --git a/view/tpl/wiki_page_history.tpl b/view/tpl/wiki_page_history.tpl
index 6ce3ce204..fef5d93a5 100644
--- a/view/tpl/wiki_page_history.tpl
+++ b/view/tpl/wiki_page_history.tpl
@@ -1,12 +1,52 @@
+<style>
+ .diff td{
+ padding:0 0.667em;
+ vertical-align:top;
+ white-space:pre;
+ white-space:pre-wrap;
+ font-family:Consolas,'Courier New',Courier,monospace;
+ font-size:1.0em;
+ line-height:1.333;
+ }
+
+ .diff span{
+ display:block;
+ min-height:1.333em;
+ margin-top:-1px;
+ padding:0 3px;
+ }
+
+ * html .diff span{
+ height:1.333em;
+ }
+
+ .diff span:first-child{
+ margin-top:0;
+ }
+
+ .diffDeleted span{
+ border:1px solid rgb(255,192,192);
+ background:rgb(255,224,224);
+ }
+
+ .diffInserted span{
+ border:1px solid rgb(192,255,192);
+ background:rgb(224,255,224);
+ }
+</style>
<table class="table-striped table-responsive table-hover" style="width: 100%;">
{{foreach $pageHistory as $commit}}
<tr><td>
<table id="rev-{{$commit.hash}}" onclick="$('#details-{{$commit.hash}}').show()" width="100%">
- <tr><td width="10%">Date</td><td width="70%">{{$commit.date}}</td><td rowspan="3" width="20%" align="right">
- <button id="revert-{{$commit.hash}}" class="btn btn-danger btn-xs" onclick="wiki_revert_page('{{$commit.hash}}')">Revert</button></td></tr>
+ <tr><td width="10%">Date</td><td width="70%">{{$commit.date}}</td>
+ <td rowspan="3" width="20%" align="right">
+ <button id="revert-{{$commit.hash}}" class="btn btn-danger btn-xs" onclick="wiki_revert_page('{{$commit.hash}}')">Revert</button>
+ <br><br>
+ <button id="compare-{{$commit.hash}}" class="btn btn-warning btn-xs" onclick="wiki_compare_page('{{$commit.hash}}')">Compare</button>
+ </td></tr>
<tr><td>Name</td><td>{{$commit.name}} &lt;{{$commit.email}}&gt;</td></tr>
<tr><td>Message</td><td>{{$commit.title}}</td></tr>
</table>
</td></tr>
{{/foreach}}
-</table> \ No newline at end of file
+</table>