boxnotes2html

Convert Box's proprietary Box Notes to HTML, Markdown, or plain text
Log | Files | Refs | README | LICENSE

commit 82eb46e6a15fc5d208e154323288007c5a80418d
parent 2a58c8a89a11d7671b94ea21d1f122e7286716ce
Author: Alex Hayes <alex.hayes@rea-group.com>
Date:   Wed, 29 Jul 2020 12:47:12 +1000

Support for tables in Markdown.

Diffstat:
MREADME.md | 4+++-
Mboxnotes2html/boxnote.py | 103++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Aboxnotes2html/table.py | 43+++++++++++++++++++++++++++++++++++++++++++
Mtests/fixtures/normal note.boxnote | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Atests/fixtures/same-line-formatting.boxnote | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/fixtures/simple_note.boxnote | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Atests/fixtures/table-aligned.boxnote | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/fixtures/table-multiline.boxnote | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/fixtures/table-simple.boxnote | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_cli.py | 20++++++++++++++++++++
Atests/test_table.py | 43+++++++++++++++++++++++++++++++++++++++++++
11 files changed, 804 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md @@ -64,14 +64,16 @@ Functioning: * Text formatting (bold, underline, colors, size, etc) * Hyperlinks * Ordered, unordered, checked and unchecked lists are supported in Markdown (using Github flavoured check/uncheck syntax). +* Tables in Markdown Caveats: * HTML lists don't supported nesting -* Tables are broken and will just be converted to plaintext. If you can figure out a clean way to do this, please submit a pull request. +* HTML tables are broken however it should be possible to take the approach used for Markdown and apply it - PRs welcome. * Images are just a link to the image in Box, converting them would require API access. * Comments and annotations are not saved. * Document history is not preserved * This tool is in ALPHA, bugs may exist. Please report any issues you encounter! +* Links to other Box notes are not changed in any way If this tool is unsatisfactory to your needs, please contact Box and tell them to build this much-needed feature! diff --git a/boxnotes2html/boxnote.py b/boxnotes2html/boxnote.py @@ -7,7 +7,10 @@ import re from functools import reduce from xml.etree import ElementTree as ET +import typing + from . import html, markdown +from .table import Table dir_path = os.path.dirname(os.path.realpath(__file__)) @@ -68,10 +71,19 @@ class FormattedText: return tags def get_table_info(self): + table_id = row_id = column_id = None for box_attribute in self.attributes: if html.get_table_info(box_attribute)[0]: - return html.get_table_info(box_attribute) - return None, None, None + table = html.get_table_info(box_attribute) + if table_id and table[0] != table_id: + raise NotImplementedError(f"Encountered table id {table[0]} but was expecting {table_id}") + + table_id = table[0] + if table[1]: + row_id = table[1] + if table[2]: + column_id = table[2] + return table_id, row_id, column_id def get_list_info(self): # refactor for box_attribute in self.attributes: @@ -212,11 +224,90 @@ class BoxNote: return css + body def as_markdown(self): - out = "" + """ + Return this note as markdown. + + ## Notes about tables + + 1. A new row starts with start with `struct-table[hash]_col[hash]` and then `struct-table[hash]_row[hash]`. + 2. The continuation of a row can be identified by `struct-table[hash]_row[hash]` and then `struct-table[hash]_col[hash]` + 3. The FormattedText that appears directly before a `struct-table[hash]_col|row[hash]` contains the content for + the cell. + 4. There doesn't appear to be an indication of a header row + 5. There can be multiple blobs of data before the `struct-table[hash]_col|row[hash]` and therefore you need to + capture this data so that it can be inserted into a table cell. + 6. BUT... there doesn't seem to be any indication that a table has finished. In some cases the last table cell + will contain a \n\n but not always. + Thus, this is why in this method we add the data to the stack, but then if we detect there is a table cell + that hasn't been filled, we fill it with any data since the previously encountered table cell. + """ + + #: A dict of data that makes up the box note. + # + # The key will either be; + # 1. An integer derived from the index of blobs; or + # 2. A string that references a table_id derived from the Box Note attribute + # `struct-table[table-id]_col|row[hash]` + out: typing.Dict[typing.Union[int, str], typing.Union[str, Table]] = {} + + #: A list of blob indexes that are captured so they can be placed within a table cell upon discovery. + captures: typing.List[int] = [] + blobs = self._get_formatted_text_list() - for blob in blobs: - out += blob.styles_to_markdown_string() - return out + + for i, blob in enumerate(blobs): + if blob.table_id: + # This blob contains reference to a table + # + # Some previously captured data forms the data that will be placed within this particular table cell. + + if blob.table_id not in out: + # This is the first time we've come across this table, so creat an instance of Table in which + # we can start to place data in. + out[blob.table_id] = Table() + + # Combine any text previously captured together. + data = ''.join([ + out.pop(capture) + for capture in captures + ]) + + if len(data) > 0: + # Add the previously captured data to the table + # + # Table relies on a dictionary (which now honours the insertion order) to ensure that this data + # will be in the correct row/column and that that row/column is rendered in the correct place + # on output. + out[blob.table_id].add_data(blob.row_id, blob.column_id, data) + + captures = [] + + else: + if blob.num_linebreaks == 0: + # Capture a reference to this data in case it needs to be placed inside a table cell + captures.append(i) + else: + # We reset the capture when there is a line break. + captures = [] + + out[i] = blob.styles_to_markdown_string() + + doc = ''.join([ + o.render_markdown() if hasattr(o, "render_markdown") else o + for o in out.values() + ]) + + # TODO: Better support for cleaning up the doc + cleanup = ( + # Box Notes can give you text in a table cell like `**H****ello**` - this is invalid Markdown and we want + # to convert it to `**Hello**`. + ('****', ''), + ) + + for search, replace in cleanup: + doc = doc.replace(search, replace) + + return doc def as_text(self): return self.text diff --git a/boxnotes2html/table.py b/boxnotes2html/table.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" +Generate markdown tables programmatically. +""" + + +class Table: + matrix = {} + + def __init__(self): + self.matrix = {} + + def render_markdown(self): + """ + Render the table as markdown + """ + out = [] + headers = 0 + for row_key, col in self.matrix.items(): + for cell_key, cell in col.items(): + if headers is not False: + headers += 1 + out.append(f"| {cell} ") + + out.append("|\n") + + if headers is not False and headers > 0: + out.append("| :-- " * headers) + out.append("|\n") + headers = False + + return "".join(out) + + def add_data(self, row, cell, data): + if row not in self.matrix: + self.matrix[row] = {} + + quoted = data.replace("\n", "<br>") + + if cell in self.matrix[row]: + self.matrix[row][cell] += f"<br>{quoted}" + else: + self.matrix[row][cell] = quoted diff --git a/tests/fixtures/normal note.boxnote b/tests/fixtures/normal note.boxnote @@ -1 +1,137 @@ -{"head":117,"savepointDataFileId":"400025482140","savepointListObject":{"80":{"revisionId":80,"timestamp":1549845414056,"state":"saved","type":"session","diffAuthorList":{"3960991723":true}}},"lastEditTimestamp":1549846860417,"diffChangeset":"Z:3i5<2nq*4*9|1-8*4*e*5*f+6|1=1=14*4*k*4=b=7s*4*9|4-11n|1=1*4*8=1*4*9-6|1=2s*4*8=1=17*4*9-1k|1=1*4*c=1=13*4*e*4=t|1=w*4*d=1=46*4*l*4=w|1=e*4*c=1|1=31*4*d=1|1=2y*4*j=1=1i*4*9|7-193*4*9-b8|4=f*4*m*4=1$HEADER","invalidDiffChangeset":false,"authorList":{"3960991723":{"authorName":"Alex Wennerberg","authorCustomAvatarUrl":"/users/3960991723/avatar"}},"diffAuthorList":{"3960991723":true},"shouldCreateSavepointBeforeApplyingNextRevision":false,"firstKeyRevision":5,"atext":{"text":"HEADER\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Duis in lorem est. Nunc ac lectus eget nibh iaculis hendrerit vitae in lectus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi consectetur nunc leo, id sollicitudin nibh blandit a. Donec euismod mollis nisl quis vehicula. Donec \n*ex maximus tortor, ac convallis nisl lorem laoreet nisi. Sed ullamcorper purus porttitor convallis \n*eleifend. Vivamus venenatis vestibulum odio\n*Donec vehicula lacus ut nisi suscipit, sit amet cursus mauris varius. Aenean consectetur fermentum \n*metus, eu faucibus ex luctus eget. Sed consectetur metus sit amet nisl fermentum, in consectetur justvenenatis. Donec ipsum quam, tempor quis arcu a, tincidunt vehicula neque. Nullam eget ligula \n*venenatis, sollicitudin neque non, iaculis ipsum. Phasellus eu nunc nec dui lobortis facilisis. Nullam quis \n*augue et massa consequat tincidunt. Vestibulum sit amet libero augue. Praesent condimentum sed ligula eu \n*viverra. Praesent bibendum dapibus erat vitae posuere.\n\nTO DO\n*lorem\n*ipsum\n*lorem2\n*impsum2\n\n","attribs":"*4*e*5*f+6*4|1+1*4+14*4*k+b*4|1+7t*4*2*7*3*8+1*4*b*6+2r*4|1+1*4*2*a*3*8+1*4*b*6+17*4|1+1*4*2*a*3*c+1*4*b*6+13*4*e*b*6+t*4*b*6+v*4|1+1*4*2*a*3*d+1*4*b*6+46*4*b*6*l+w*4*b*6+d*4|1+1*4*2*7*3*c+1*4*b*6+30*4|1+1*4*2*7*3*d+1*4*b*6+2x*4|1+1*4*2*7*3*j+1*4*b*6+1i*4|2+2*4*5*f+5|1+1*4*2*i*3+1*4*5*6+5*4|1+1*4*2*m*3+1*4*5*6+5*4|1+1*4*2*i*3+1*4*5*6+6*4|1+1*4*2*i*3+1*4*5*6+7|2+2","opCount":44,"appliedAttribsCount":120,"maxAttribsOnSingleOp":5},"pool":{"numToAttrib":{"0":["author","a.4fz9s4pIrvcRKF5l"],"1":["align","left"],"2":["insertorder","first"],"3":["lmkr","1"],"4":["author","3960991723"],"5":["font-color-000000","true"],"6":["font-size-medium","true"],"7":["list","number1"],"8":["start","1"],"9":["removed","true"],"10":["list","number2"],"11":["font-color-222222","true"],"12":["start","2"],"13":["start","3"],"14":["bold","true"],"15":["font-size-large","true"],"16":["font-size-medium",""],"17":["link-MTU0OTg0NTM4NDI5MC1nb29nbGUuY29t","true"],"18":["list","unchecked1"],"19":["start","4"],"20":["link-MTU0OTg0NjgyMTM3MC1nb29nbGUuY29t","true"],"21":["italic","true"],"22":["list","checked1"]},"nextNum":23},"chatHead":-1,"publicStatus":false,"passwordHash":null,"savedRevisions":[]} -\ No newline at end of file +{ + "head": 117, + "savepointDataFileId": "400025482140", + "savepointListObject": { + "80": { + "revisionId": 80, + "timestamp": 1549845414056, + "state": "saved", + "type": "session", + "diffAuthorList": { + "3960991723": true + } + } + }, + "lastEditTimestamp": 1549846860417, + "diffChangeset": "Z:3i5<2nq*4*9|1-8*4*e*5*f+6|1=1=14*4*k*4=b=7s*4*9|4-11n|1=1*4*8=1*4*9-6|1=2s*4*8=1=17*4*9-1k|1=1*4*c=1=13*4*e*4=t|1=w*4*d=1=46*4*l*4=w|1=e*4*c=1|1=31*4*d=1|1=2y*4*j=1=1i*4*9|7-193*4*9-b8|4=f*4*m*4=1$HEADER", + "invalidDiffChangeset": false, + "authorList": { + "3960991723": { + "authorName": "Alex Wennerberg", + "authorCustomAvatarUrl": "/users/3960991723/avatar" + } + }, + "diffAuthorList": { + "3960991723": true + }, + "shouldCreateSavepointBeforeApplyingNextRevision": false, + "firstKeyRevision": 5, + "atext": { + "text": "HEADER\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Duis in lorem est. Nunc ac lectus eget nibh iaculis hendrerit vitae in lectus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi consectetur nunc leo, id sollicitudin nibh blandit a. Donec euismod mollis nisl quis vehicula. Donec \n*ex maximus tortor, ac convallis nisl lorem laoreet nisi. Sed ullamcorper purus porttitor convallis \n*eleifend. Vivamus venenatis vestibulum odio\n*Donec vehicula lacus ut nisi suscipit, sit amet cursus mauris varius. Aenean consectetur fermentum \n*metus, eu faucibus ex luctus eget. Sed consectetur metus sit amet nisl fermentum, in consectetur justvenenatis. Donec ipsum quam, tempor quis arcu a, tincidunt vehicula neque. Nullam eget ligula \n*venenatis, sollicitudin neque non, iaculis ipsum. Phasellus eu nunc nec dui lobortis facilisis. Nullam quis \n*augue et massa consequat tincidunt. Vestibulum sit amet libero augue. Praesent condimentum sed ligula eu \n*viverra. Praesent bibendum dapibus erat vitae posuere.\n\nTO DO\n*lorem\n*ipsum\n*lorem2\n*impsum2\n\n", + "attribs": "*4*e*5*f+6*4|1+1*4+14*4*k+b*4|1+7t*4*2*7*3*8+1*4*b*6+2r*4|1+1*4*2*a*3*8+1*4*b*6+17*4|1+1*4*2*a*3*c+1*4*b*6+13*4*e*b*6+t*4*b*6+v*4|1+1*4*2*a*3*d+1*4*b*6+46*4*b*6*l+w*4*b*6+d*4|1+1*4*2*7*3*c+1*4*b*6+30*4|1+1*4*2*7*3*d+1*4*b*6+2x*4|1+1*4*2*7*3*j+1*4*b*6+1i*4|2+2*4*5*f+5|1+1*4*2*i*3+1*4*5*6+5*4|1+1*4*2*m*3+1*4*5*6+5*4|1+1*4*2*i*3+1*4*5*6+6*4|1+1*4*2*i*3+1*4*5*6+7|2+2", + "opCount": 44, + "appliedAttribsCount": 120, + "maxAttribsOnSingleOp": 5 + }, + "pool": { + "numToAttrib": { + "0": [ + "author", + "a.4fz9s4pIrvcRKF5l" + ], + "1": [ + "align", + "left" + ], + "2": [ + "insertorder", + "first" + ], + "3": [ + "lmkr", + "1" + ], + "4": [ + "author", + "3960991723" + ], + "5": [ + "font-color-000000", + "true" + ], + "6": [ + "font-size-medium", + "true" + ], + "7": [ + "list", + "number1" + ], + "8": [ + "start", + "1" + ], + "9": [ + "removed", + "true" + ], + "10": [ + "list", + "number2" + ], + "11": [ + "font-color-222222", + "true" + ], + "12": [ + "start", + "2" + ], + "13": [ + "start", + "3" + ], + "14": [ + "bold", + "true" + ], + "15": [ + "font-size-large", + "true" + ], + "16": [ + "font-size-medium", + "" + ], + "17": [ + "link-MTU0OTg0NTM4NDI5MC1nb29nbGUuY29t", + "true" + ], + "18": [ + "list", + "unchecked1" + ], + "19": [ + "start", + "4" + ], + "20": [ + "link-MTU0OTg0NjgyMTM3MC1nb29nbGUuY29t", + "true" + ], + "21": [ + "italic", + "true" + ], + "22": [ + "list", + "checked1" + ] + }, + "nextNum": 23 + }, + "chatHead": -1, + "publicStatus": false, + "passwordHash": null, + "savedRevisions": [] +} diff --git a/tests/fixtures/same-line-formatting.boxnote b/tests/fixtures/same-line-formatting.boxnote @@ -0,0 +1,83 @@ +{ + "head": 29, + "savepointDataFileId": "690977068418", + "savepointListObject": {}, + "lastEditTimestamp": 1594722653948, + "diffChangeset": "Z:1>w*4*8*5*6+4*4*5*6+1*4*5*6*9+6*4*5*6+1*4*5*6*a+9|1+1*4*5*6+5*4*5*6*b+4*4|1+1$Bold italic underline\nNext line\n", + "invalidDiffChangeset": false, + "authorList": { + "3095298546": { + "authorName": "Alex Hayes", + "authorCustomAvatarUrl": "/users/3095298546/avatar" + } + }, + "diffAuthorList": { + "3095298546": true + }, + "shouldCreateSavepointBeforeApplyingNextRevision": false, + "firstKeyRevision": 5, + "atext": { + "text": "Bold italic underline\nNext line\n\n", + "attribs": "*4*8*5*6+4*4*5*6+1*4*5*6*9+6*4*5*6+1*4*5*6*a+9|1+1*4*5*6+5*4*5*6*b+4*4|1+1|1+1", + "opCount": 10, + "appliedAttribsCount": 26, + "maxAttribsOnSingleOp": 4 + }, + "pool": { + "numToAttrib": { + "0": [ + "author", + "a.4fz9s4pIrvcRKF5l" + ], + "1": [ + "align", + "left" + ], + "2": [ + "insertorder", + "first" + ], + "3": [ + "lmkr", + "1" + ], + "4": [ + "author", + "3095298546" + ], + "5": [ + "font-color-000000", + "true" + ], + "6": [ + "font-size-medium", + "true" + ], + "7": [ + "removed", + "true" + ], + "8": [ + "bold", + "true" + ], + "9": [ + "italic", + "true" + ], + "10": [ + "underline", + "true" + ], + "11": [ + "strikethrough", + "true" + ] + }, + "nextNum": 12 + }, + "chatHead": -1, + "publicStatus": false, + "passwordHash": null, + "savedRevisions": [] +} diff --git a/tests/fixtures/simple_note.boxnote b/tests/fixtures/simple_note.boxnote @@ -1 +1,63 @@ -{"head":11,"savepointDataFileId":"343756088721","savepointListObject":{},"lastEditTimestamp":1541287851717,"diffChangeset":"Z:1>f*4*5*6+d|1+1*4|1+1$Hello, World!\n\n","invalidDiffChangeset":false,"authorList":{"3960991723":{"authorName":"Alex Wennerberg","authorCustomAvatarUrl":"/users/3960991723/avatar"}},"diffAuthorList":{"3960991723":true},"shouldCreateSavepointBeforeApplyingNextRevision":false,"firstKeyRevision":5,"atext":{"text":"Hello, World!\n\n\n","attribs":"*4*5*6+d|1+1*4|1+1|1+1","opCount":4,"appliedAttribsCount":4,"maxAttribsOnSingleOp":3},"pool":{"numToAttrib":{"0":["author","a.4fz9s4pIrvcRKF5l"],"1":["align","left"],"2":["insertorder","first"],"3":["lmkr","1"],"4":["author","3960991723"],"5":["font-color-000000","true"],"6":["font-size-medium","true"]},"nextNum":7},"chatHead":-1,"publicStatus":false,"passwordHash":null,"savedRevisions":[]} -\ No newline at end of file +{ + "head": 11, + "savepointDataFileId": "343756088721", + "savepointListObject": {}, + "lastEditTimestamp": 1541287851717, + "diffChangeset": "Z:1>f*4*5*6+d|1+1*4|1+1$Hello, World!\n\n", + "invalidDiffChangeset": false, + "authorList": { + "3960991723": { + "authorName": "Alex Wennerberg", + "authorCustomAvatarUrl": "/users/3960991723/avatar" + } + }, + "diffAuthorList": { + "3960991723": true + }, + "shouldCreateSavepointBeforeApplyingNextRevision": false, + "firstKeyRevision": 5, + "atext": { + "text": "Hello, World!\n\n\n", + "attribs": "*4*5*6+d|1+1*4|1+1|1+1", + "opCount": 4, + "appliedAttribsCount": 4, + "maxAttribsOnSingleOp": 3 + }, + "pool": { + "numToAttrib": { + "0": [ + "author", + "a.4fz9s4pIrvcRKF5l" + ], + "1": [ + "align", + "left" + ], + "2": [ + "insertorder", + "first" + ], + "3": [ + "lmkr", + "1" + ], + "4": [ + "author", + "3960991723" + ], + "5": [ + "font-color-000000", + "true" + ], + "6": [ + "font-size-medium", + "true" + ] + }, + "nextNum": 7 + }, + "chatHead": -1, + "publicStatus": false, + "passwordHash": null, + "savedRevisions": [] +} diff --git a/tests/fixtures/table-aligned.boxnote b/tests/fixtures/table-aligned.boxnote @@ -0,0 +1,99 @@ +{ + "head": 7, + "savepointDataFileId": "694007942494", + "savepointListObject": {}, + "lastEditTimestamp": 1595366506717, + "diffChangeset": "Z:1>1q*4|1+1*4*5*6*7+c*4*8*9|1+1*a*4*2*3+1*4*5*6*7+e*4*b*9|1+1*c*4*2*3+1*4*5*7+1*4*5*6*7+c*4*d*9|1+1*1*4*2*3+1*4*6*7+3*4*8*e|1+1*a*4*2*3+1*4*6*7+3*4*b*e|1+1*c*4*2*3+1*4*6*7+3*4*d*e|1+1*c*4*2*3+1|1+1$\nLeft Aligned\n*Center Aligned\n*Right Aligned\n*1:1\n*1:2\n*1:3\n*\n", + "invalidDiffChangeset": false, + "authorList": { + "3095298546": { + "authorName": "Alex Hayes", + "authorCustomAvatarUrl": "/users/3095298546/avatar" + } + }, + "diffAuthorList": { + "3095298546": true + }, + "shouldCreateSavepointBeforeApplyingNextRevision": false, + "firstKeyRevision": 5, + "atext": { + "text": "\nLeft Aligned\n*Center Aligned\n*Right Aligned\n*1:1\n*1:2\n*1:3\n*\n\n", + "attribs": "*4|1+1*4*5*6*7+c*4*8*9|1+1*a*4*2*3+1*4*5*6*7+e*4*b*9|1+1*c*4*2*3+1*4*5*7+1*4*5*6*7+c*4*d*9|1+1*1*4*2*3+1*4*6*7+3*4*8*e|1+1*a*4*2*3+1*4*6*7+3*4*b*e|1+1*c*4*2*3+1*4*6*7+3*4*d*e|1+1*c*4*2*3+1|2+2", + "opCount": 21, + "appliedAttribsCount": 67, + "maxAttribsOnSingleOp": 4 + }, + "pool": { + "numToAttrib": { + "0": [ + "author", + "a.4fz9s4pIrvcRKF5l" + ], + "1": [ + "align", + "left" + ], + "2": [ + "insertorder", + "first" + ], + "3": [ + "lmkr", + "1" + ], + "4": [ + "author", + "3095298546" + ], + "5": [ + "bold", + "true" + ], + "6": [ + "font-color-000000", + "true" + ], + "7": [ + "font-size-medium", + "true" + ], + "8": [ + "struct-table66e3964d35a14dd5955daccba7df6af2_col8a55817313a5466686e97c6efdc21f32", + "true" + ], + "9": [ + "struct-table66e3964d35a14dd5955daccba7df6af2_rowc99aa4d521f4432080e3b991361c8c88", + "true" + ], + "10": [ + "align", + "center" + ], + "11": [ + "struct-table66e3964d35a14dd5955daccba7df6af2_cole09dca266fae4c0b80e699668778ab10", + "true" + ], + "12": [ + "align", + "right" + ], + "13": [ + "struct-table66e3964d35a14dd5955daccba7df6af2_colac4fb7416fc44966a5886df322f79ebc", + "true" + ], + "14": [ + "struct-table66e3964d35a14dd5955daccba7df6af2_row3d13a40994d44c7ca857d117609d6789", + "true" + ], + "15": [ + "removed", + "true" + ] + }, + "nextNum": 16 + }, + "chatHead": -1, + "publicStatus": false, + "passwordHash": null, + "savedRevisions": [] +} diff --git a/tests/fixtures/table-multiline.boxnote b/tests/fixtures/table-multiline.boxnote @@ -0,0 +1,117 @@ +{ + "head": 68, + "savepointDataFileId": "690895910333", + "savepointListObject": { + "40": { + "revisionId": 40, + "timestamp": 1594704759431, + "state": "saved", + "type": "session", + "diffAuthorList": { + "3095298546": true + } + } + }, + "lastEditTimestamp": 1595366088001, + "diffChangeset": "Z:1e>i|1=1*4*f*c*d+7*4*f*5*g|1+1*4*f*c*d+e*4*5*g|1+1*4*f*c*d+9*4*5*g|1+1*4*f*c*d+4*4*5*g|1+1*4*f*c*d+7*4*f*7*g|1+1*4*f*c*d+7*8*g|1+1*4*h*4|2=8*4*h*4=3|1=1*4*e|9-10$Title 1\nMultiple lines\nLook like\nthis\nTitle 2\nTitle 3\n", + "invalidDiffChangeset": false, + "authorList": { + "3095298546": { + "authorName": "Alex Hayes", + "authorCustomAvatarUrl": "/users/3095298546/avatar" + } + }, + "diffAuthorList": { + "3095298546": true + }, + "shouldCreateSavepointBeforeApplyingNextRevision": false, + "firstKeyRevision": 5, + "atext": { + "text": "\nTitle 1\nMultiple lines\nLook like\nthis\nTitle 2\nTitle 3\n1:1\n1:2\n1:3\n\n", + "attribs": "*4|1+1*4*f*c*d+7*4*f*5*g|1+1*4*f*c*d+e*4*5*g|1+1*4*f*c*d+9*4*5*g|1+1*4*f*c*d+4*4*5*g|1+1*4*f*c*d+7*4*f*7*g|1+1*4*f*c*d+7*8*g|1+1*4*c*d+3*4*5*6|1+1*4*c*d+3*4*7*6|1+1*4*c*d+3*4*8*6|1+1|1+1", + "opCount": 20, + "appliedAttribsCount": 62, + "maxAttribsOnSingleOp": 4 + }, + "pool": { + "numToAttrib": { + "0": [ + "author", + "a.4fz9s4pIrvcRKF5l" + ], + "1": [ + "align", + "left" + ], + "2": [ + "insertorder", + "first" + ], + "3": [ + "lmkr", + "1" + ], + "4": [ + "author", + "3095298546" + ], + "5": [ + "struct-tableb7c669166c2944818c015a66c2c74463_col571b2af156474d108e5b04a5276bed46", + "true" + ], + "6": [ + "struct-tableb7c669166c2944818c015a66c2c74463_row05f8b48986bd40e0abd8e22aa4bbb647", + "true" + ], + "7": [ + "struct-tableb7c669166c2944818c015a66c2c74463_col9d6b438ed9f048afba4cc2fb378b703c", + "true" + ], + "8": [ + "struct-tableb7c669166c2944818c015a66c2c74463_col6dcdb35742584383800af4189f7e6ba6", + "true" + ], + "9": [ + "struct-tableb7c669166c2944818c015a66c2c74463_rowc17a390ff86241af9d38996051ccba30", + "true" + ], + "10": [ + "struct-tableb7c669166c2944818c015a66c2c74463_rowb2f7814535104e129c5e55295d6f2205", + "true" + ], + "11": [ + "struct-tableb7c669166c2944818c015a66c2c74463_rowfd898b77f27042c79a8908880a75d853", + "true" + ], + "12": [ + "font-color-000000", + "true" + ], + "13": [ + "font-size-medium", + "true" + ], + "14": [ + "removed", + "true" + ], + "15": [ + "bold", + "true" + ], + "16": [ + "struct-tableb7c669166c2944818c015a66c2c74463_row9dfe923977904f17918e1a911ed327b9", + "true" + ], + "17": [ + "bold", + "" + ] + }, + "nextNum": 18 + }, + "chatHead": -1, + "publicStatus": false, + "passwordHash": null, + "savedRevisions": [] +} diff --git a/tests/fixtures/table-simple.boxnote b/tests/fixtures/table-simple.boxnote @@ -0,0 +1,99 @@ +{ + "head": 40, + "savepointDataFileId": "690895910333", + "savepointListObject": {}, + "lastEditTimestamp": 1594704759431, + "diffChangeset": "Z:1>1d*4|1+1*4*f*c*d+3*4*f*5*6|1+1*4*f*c*d+3*4*f*7*6|1+1*4*f*c*d+3*4*8*6|1+1*4*c*d+3*4*5*9|1+1*4*c*d+3*4*7*9|1+1*4*c*d+3*4*8*9|1+1*4*c*d+3*4*5*a|1+1*4*c*d+3*4*7*a|1+1*4*c*d+3*4*8*a|1+1*4*c*d+3*4*5*b|1+1*4*c*d+3*4*7*b|1+1*4*c*d+3*4*8*b|1+1$\n1:1\n1:2\n1:3\n2:1\n2:2\n2:3\n3:1\n3:2\n3:3\n4:1\n4:2\n4:3\n", + "invalidDiffChangeset": false, + "authorList": { + "3095298546": { + "authorName": "Alex Hayes", + "authorCustomAvatarUrl": "/users/3095298546/avatar" + } + }, + "diffAuthorList": { + "3095298546": true + }, + "shouldCreateSavepointBeforeApplyingNextRevision": false, + "firstKeyRevision": 5, + "atext": { + "text": "\n1:1\n1:2\n1:3\n2:1\n2:2\n2:3\n3:1\n3:2\n3:3\n4:1\n4:2\n4:3\n\n", + "attribs": "*4|1+1*4*f*c*d+3*4*f*5*6|1+1*4*f*c*d+3*4*f*7*6|1+1*4*f*c*d+3*4*8*6|1+1*4*c*d+3*4*5*9|1+1*4*c*d+3*4*7*9|1+1*4*c*d+3*4*8*9|1+1*4*c*d+3*4*5*a|1+1*4*c*d+3*4*7*a|1+1*4*c*d+3*4*8*a|1+1*4*c*d+3*4*5*b|1+1*4*c*d+3*4*7*b|1+1*4*c*d+3*4*8*b|1+1|1+1", + "opCount": 26, + "appliedAttribsCount": 78, + "maxAttribsOnSingleOp": 4 + }, + "pool": { + "numToAttrib": { + "0": [ + "author", + "a.4fz9s4pIrvcRKF5l" + ], + "1": [ + "align", + "left" + ], + "2": [ + "insertorder", + "first" + ], + "3": [ + "lmkr", + "1" + ], + "4": [ + "author", + "3095298546" + ], + "5": [ + "struct-tableb7c669166c2944818c015a66c2c74463_col571b2af156474d108e5b04a5276bed46", + "true" + ], + "6": [ + "struct-tableb7c669166c2944818c015a66c2c74463_row05f8b48986bd40e0abd8e22aa4bbb647", + "true" + ], + "7": [ + "struct-tableb7c669166c2944818c015a66c2c74463_col9d6b438ed9f048afba4cc2fb378b703c", + "true" + ], + "8": [ + "struct-tableb7c669166c2944818c015a66c2c74463_col6dcdb35742584383800af4189f7e6ba6", + "true" + ], + "9": [ + "struct-tableb7c669166c2944818c015a66c2c74463_rowc17a390ff86241af9d38996051ccba30", + "true" + ], + "10": [ + "struct-tableb7c669166c2944818c015a66c2c74463_rowb2f7814535104e129c5e55295d6f2205", + "true" + ], + "11": [ + "struct-tableb7c669166c2944818c015a66c2c74463_rowfd898b77f27042c79a8908880a75d853", + "true" + ], + "12": [ + "font-color-000000", + "true" + ], + "13": [ + "font-size-medium", + "true" + ], + "14": [ + "removed", + "true" + ], + "15": [ + "bold", + "true" + ] + }, + "nextNum": 16 + }, + "chatHead": -1, + "publicStatus": false, + "passwordHash": null, + "savedRevisions": [] +} diff --git a/tests/test_cli.py b/tests/test_cli.py @@ -14,3 +14,23 @@ def test_everything(): for txtfmt in "md", "txt", "html": args = ["tests/fixtures", "-f", txtfmt] cli.run_with_args(args) + + +def test_table_simple(): + args = ["tests/fixtures/table-simple.boxnote", "-f", "md"] + cli.run_with_args(args) + + +def test_table_multiline(): + args = ["tests/fixtures/table-multiline.boxnote", "-f", "md"] + cli.run_with_args(args) + + +def test_table_aligned(): + args = ["tests/fixtures/table-aligned.boxnote", "-f", "md"] + cli.run_with_args(args) + + +def test_same_line_formatting(): + args = ["tests/fixtures/same-line-formatting.boxnote", "-f", "md"] + cli.run_with_args(args) diff --git a/tests/test_table.py b/tests/test_table.py @@ -0,0 +1,43 @@ +from boxnotes2html.table import Table + + +class TestTable: + + def test_render_markdown(self): + table = Table() + table.add_data(1, 1, "Name") + table.add_data(1, 2, "Country") + table.add_data(1, 3, "Birthdate") + + table.add_data(2, 1, "Jill") + table.add_data(2, 2, "Australia") + table.add_data(2, 3, "2000-01-01") + + table.add_data(3, 1, "Alfonse") + table.add_data(3, 2, "Chile") + table.add_data(3, 3, "1981-02-04") + + expected = "| Name | Country | Birthdate |\n" + \ + "| :-- | :-- | :-- |\n" + \ + "| Jill | Australia | 2000-01-01 |\n" + \ + "| Alfonse | Chile | 1981-02-04 |\n" + + assert table.render_markdown() == expected + + def test_append_data_markdown(self): + """ + Test that appending data to add_data works as expected and renders multiple lines properly. + """ + table = Table() + table.add_data(1, 1, "Name") + table.add_data(1, 1, "(Full name)") + table.add_data(1, 1, "(but with no spaces)") + table.add_data(1, 1, "(as in full name camelcase)") + + table.add_data(2, 1, "JillFromDownUnder") + + expected = "| Name<br>(Full name)<br>(but with no spaces)<br>(as in full name camelcase) |\n" + \ + "| :-- |\n" + \ + "| JillFromDownUnder |\n" + + assert table.render_markdown() == expected