def test_info_and_referrers(fs): doc = """--- title: A Note created: 2012-01-02 03:04:05 ... I link to [two](two.md) and [three](../otherdir/three.md#heading) and have #two #tags.""" path1 = '/notes/dir/one.md' path2 = '/notes/dir/two.md' path3 = '/notes/otherdir/three.md' fs.create_file(path1, contents=doc) fs.create_file(path2, contents='---\ntitle: Note 2\n...\n') repo = config().instantiate() assert repo.info(path1, FileInfoReq.full()) == FileInfo( path1, title='A Note', created=datetime(2012, 1, 2, 3, 4, 5), tags={'tags', 'two'}, links=[ LinkInfo(path1, h) for h in ['../otherdir/three.md#heading', 'two.md'] ]) assert repo.info(path2, FileInfoReq.full()) == FileInfo( path2, title='Note 2', backlinks=[LinkInfo(path1, 'two.md')]) assert repo.info(path3, FileInfoReq.full()) == FileInfo( path3, backlinks=[LinkInfo(path1, '../otherdir/three.md#heading')])
def test_change(fs): fs.cwd = '/notes' path1 = '/notes/one.md' path2 = '/notes/two.md' path3 = '/notes/moved.md' fs.create_file(path1, contents='[1](old)') fs.create_file(path2, contents='[2](foo)') edits = [SetTitleCmd(path1, 'New Title'), ReplaceHrefCmd(path1, 'old', 'new'), MoveCmd(path1, path3), ReplaceHrefCmd(path2, 'foo', 'bar')] repo = config().instantiate() repo.change(edits) assert not Path(path1).exists() assert Path(path3).read_text() == '---\ntitle: New Title\n...\n[1](new)' assert Path(path2).read_text() == '[2](bar)' assert repo.info(path1, FileInfoReq.full()) == FileInfo(path1) assert repo.info(path3, FileInfoReq.full()) == FileInfo(path3, title='New Title', links=[LinkInfo(path3, 'new')]) assert repo.info(path2, FileInfoReq.full()) == FileInfo(path2, links=[LinkInfo(path2, 'bar')]) assert repo.info('old', FileInfoReq.full()) == FileInfo('/notes/old') assert repo.info('foo', FileInfoReq.full()) == FileInfo('/notes/foo') assert repo.info('new', FileInfoReq.full()) == FileInfo('/notes/new', backlinks=[LinkInfo(path3, 'new')]) assert repo.info('bar', FileInfoReq.full()) == FileInfo('/notes/bar', backlinks=[LinkInfo(path2, 'bar')]) # regression test for bug where invalidate removed entries for files that were referred to # only by files that had not been changed repo.invalidate() assert repo.info('new', FileInfoReq.full()) == FileInfo('/notes/new', backlinks=[LinkInfo(path3, 'new')]) assert repo.info('bar', FileInfoReq.full()) == FileInfo('/notes/bar', backlinks=[LinkInfo(path2, 'bar')])
def test_ignore_fenced_code_blocks(fs): doc = """#tag1 [link](link1.md) ```foo #tag2 #tag3 text [link](link1.md) ``` #tag3 [link](link2.md) ``` [link](link3.md) ```""" path = '/fakenotes/text.md' fs.create_file(path, contents=doc) acc = MarkdownAccessor(path) info = acc.info() assert info.tags == {'tag1', 'tag3'} assert info.links == [ LinkInfo(path, 'link1.md'), LinkInfo(path, 'link2.md') ] acc.edit(ReplaceHrefCmd(path, 'link1.md', 'CHANGED1')) acc.edit(ReplaceHrefCmd(path, 'link2.md', 'CHANGED2')) acc.edit(ReplaceHrefCmd(path, 'link3.md', 'CHANGED3')) acc.edit(DelTagCmd(path, 'tag3')) acc.edit(DelTagCmd(path, 'tag2')) acc.save() assert Path(path).read_text() == """#tag1 [link](CHANGED1)
def test_duplicate_links(fs): doc = """I link to [two](two.md) [two](two.md) times.""" path1 = '/notes/one.md' path2 = '/notes/two.md' fs.create_file(path1, contents=doc) repo = config().instantiate() assert repo.info(path1).links == [LinkInfo(path1, 'two.md'), LinkInfo(path1, 'two.md')] assert repo.info(path2, 'backlinks').backlinks == [LinkInfo(path1, 'two.md'), LinkInfo(path1, 'two.md')]
def test_ignore(fs): path1 = '/notes/one.md' path2 = '/notes/.two.md' fs.create_file(path1, contents='I link to [two](.two.md)') fs.create_file(path2, contents='I link to [one](one.md)') repo = DirectRepoConf(root_paths={'/notes'}).instantiate() assert list(repo.query()) == [repo.info(path1)] assert not repo.info(path1, FileInfoReq.full()).backlinks assert repo.info(path2, FileInfoReq.full()).backlinks == [LinkInfo(path1, '.two.md')] repo.conf.ignore = lambda _1, _2: False assert list(repo.query()) == [repo.info(path1), repo.info(path2)] assert repo.info(path1, FileInfoReq.full()).backlinks == [LinkInfo(path2, 'one.md')] assert repo.info(path2, FileInfoReq.full()).backlinks == [LinkInfo(path1, '.two.md')]
def test_ignore(fs): path1 = '/notes/one.md' path2 = '/notes/.two.md' fs.create_file(path1, contents='I link to [two](.two.md)') fs.create_file(path2, contents='I link to [one](one.md)') with config().instantiate() as repo: assert list(repo.query()) == [repo.info(path1)] assert not repo.info(path1, FileInfoReq.full()).backlinks assert repo.info(path2, FileInfoReq.full()).backlinks == [LinkInfo(path1, '.two.md')] conf = config() conf.ignore = lambda _1, _2: False with conf.instantiate() as repo: assert list(repo.query()) == [repo.info(path1), repo.info(path2)] assert repo.info(path1, FileInfoReq.full()).backlinks == [LinkInfo(path2, 'one.md')] assert repo.info(path2, FileInfoReq.full()).backlinks == [LinkInfo(path1, '.two.md')]
def _info(self, info: FileInfo): info.title = self.meta.get('title') info.created = self.meta.get('created') info.tags = {k.lower() for k in self.meta.get('keywords', []) }.union(self._hashtags) info.links = [LinkInfo(self.path, r.href) for r in sorted(self.hrefs)]
def test_info(fs): doc = """<html> <head> <title>I Am A Strange Knot</title> <meta name="keywords" content="mind, Philosophy, cOnsciOusNess"/> <meta name="created" content="2019-10-03 23:31:14 -0800"/> </head> <body> No #extra tags in the body for now! And <a href="#nope">this will never be a tag.</a> Here's a <a href="../Another%20Note.md">link to another note</a>, and here's an image: <img src="me.html.resources/A%20Picture.png" /> </body> </html>""" path = Path('/fakenotes/test.html') fs.create_file(path, contents=doc) info = HTMLAccessor(str(path)).info() assert info.path == str(path) assert info.title == 'I Am A Strange Knot' assert info.tags == {'mind', 'philosophy', 'consciousness'} assert info.created == datetime(2019, 10, 3, 23, 31, 14, 0, timezone(timedelta(hours=-8))) assert info.links == [ LinkInfo(str(path), href) for href in sorted([ '../Another%20Note.md', 'me.html.resources/A%20Picture.png', '#nope' ]) ]
def test_backlinks(fs): fs.cwd = '/notes/foo' fs.create_file('/notes/foo/subject.md') fs.create_file('/notes/bar/baz/r1.md', contents='[1](no) [2](../../foo/subject.md)') fs.create_file('/notes/bar/baz/no.md', contents='[3](../../foo/bogus') fs.create_file('/notes/r2.md', contents='[4](foo/subject.md) [5](foo/bogus)') fs.create_file('/notes/foo/r3.md', contents='[6](subject.md)') repo = DirectRepoConf(root_paths={'/notes'}).instantiate() info = repo.info('subject.md', 'backlinks') assert info.backlinks == [ LinkInfo('/notes/bar/baz/r1.md', '../../foo/subject.md'), LinkInfo('/notes/foo/r3.md', 'subject.md'), LinkInfo('/notes/r2.md', 'foo/subject.md'), ]
def _info(self, info: FileInfo): info.title = self._title() info.created = self._created() info.tags = self._tags() info.links = [ LinkInfo(self.path, href) for href in sorted(self._link_els.keys()) ]
def test_info(fs): doc = """--- title: An Examination of the Navel created: 2019-06-04 10:12:13-08:00 keywords: - TrulyProfound ... #personal #book-draft # Preface: Reasons for #journaling As I have explained at length in [another note](../Another%20Note.md) and also published about online (see [this article](http://example.com/blah) among many others), ... """ path = '/fakenotes/test.md' fs.create_file(path, contents=doc) info = MarkdownAccessor(path).info() assert info.path == path assert info.links == [ LinkInfo(path, r) for r in sorted(['../Another%20Note.md', 'http://example.com/blah']) ] assert info.tags == { 'trulyprofound', 'personal', 'book-draft', 'journaling' } assert info.title == 'An Examination of the Navel' assert info.created == datetime(2019, 6, 4, 10, 12, 13, 0, timezone(timedelta(hours=-8)))
def info(self, path: str, fields: FileInfoReqIsh = FileInfoReq.internal(), path_resolved=False) -> FileInfo: self._refresh_if_needed() if not path_resolved: path = os.path.abspath(path) fields = FileInfoReq.parse(fields) cursor = self.connection.cursor() cursor.execute('SELECT id, title, created FROM files WHERE path = ?', (path, )) file_row = cursor.fetchone() info = FileInfo(path) if file_row: file_id = file_row[0] info.title = file_row[1] time_field = file_row[2] if time_field: if time_field.isnumeric(): info.created = datetime.utcfromtimestamp( int(time_field) / 1000) else: info.created = datetime.fromisoformat(time_field) if fields.tags: cursor.execute('SELECT tag FROM file_tags WHERE file_id = ?', (file_id, )) info.tags = {r[0] for r in cursor} if fields.links: cursor.execute( 'SELECT href FROM file_links WHERE referrer_id = ?', (file_id, )) info.links = [ LinkInfo(path, href) for href in sorted(r[0] for r in cursor) ] if fields.backlinks: cursor.execute( 'SELECT referrers.path, file_links.href' ' FROM files referrers' ' INNER JOIN file_links ON referrers.id = file_links.referrer_id' ' WHERE file_links.referent_id = ?', (file_id, )) info.backlinks = [ LinkInfo(referrer, href) for referrer, href in cursor ] info.backlinks.sort(key=attrgetter('referrer', 'href')) return info
def test_invalidate(fs): repo = config().instantiate() path = '/notes/one.md' assert repo.info(path, FileInfoReq.full()) == FileInfo(path) fs.create_file(path, contents='#hello [link](foo.md)') assert repo.info(path, FileInfoReq.full()) == FileInfo(path) repo.invalidate() assert repo.info(path, FileInfoReq.full()) == FileInfo( path, tags={'hello'}, links=[LinkInfo(path, 'foo.md')]) repo.invalidate() Path(path).write_text('#goodbye') repo.invalidate() assert repo.info(path, FileInfoReq.full()) == FileInfo(path, tags={'goodbye'})
def test_apply_sorting(): data = [ FileInfo('/a/one', tags={'baz'}, backlinks=[LinkInfo(referrer='whatever', href='whatever')]), FileInfo('/b/two', title='Beta', created=datetime(2010, 1, 15)), FileInfo('/c/Three', title='Gamma', created=datetime(2012, 1, 9), backlinks=[ LinkInfo(referrer='whatever', href='whatever'), LinkInfo(referrer='whatever', href='whatever') ]), FileInfo('/d/four', title='delta', created=datetime(2012, 1, 9), tags={'foo', 'bar'}) ] assert FileQuery.parse('sort:path').apply_sorting(data) == data assert FileQuery.parse('sort:-path').apply_sorting(data) == list( reversed(data)) assert FileQuery.parse('sort:filename').apply_sorting(data) == [ data[3], data[0], data[2], data[1] ] assert FileQuery(sort_by=[ FileQuerySort(FileQuerySortField.FILENAME, ignore_case=False) ]).apply_sorting(data) == [data[2], data[3], data[0], data[1]] assert FileQuery.parse('sort:title').apply_sorting(data) == [ data[1], data[3], data[2], data[0] ] assert FileQuery( sort_by=[FileQuerySort(FileQuerySortField.TITLE, ignore_case=False) ]).apply_sorting(data) == [ data[1], data[2], data[3], data[0] ] assert FileQuery( sort_by=[FileQuerySort(FileQuerySortField.TITLE, missing_first=True) ]).apply_sorting(data) == [ data[0], data[1], data[3], data[2] ] assert FileQuery(sort_by=[ FileQuerySort( FileQuerySortField.TITLE, missing_first=True, reverse=True) ]).apply_sorting(data) == [data[2], data[3], data[1], data[0]] assert FileQuery.parse('sort:created').apply_sorting(data) == [ data[1], data[2], data[3], data[0] ] assert FileQuery.parse('sort:-created').apply_sorting(data) == [ data[0], data[2], data[3], data[1] ] assert FileQuery(sort_by=[ FileQuerySort(FileQuerySortField.CREATED, missing_first=True) ]).apply_sorting(data) == [data[0], data[1], data[2], data[3]] assert FileQuery.parse('sort:-tags').apply_sorting(data) == [ data[3], data[0], data[1], data[2] ] assert FileQuery.parse('sort:-backlinks').apply_sorting(data) == [ data[2], data[0], data[1], data[3] ] assert FileQuery.parse('sort:created,title').apply_sorting(data) == [ data[1], data[3], data[2], data[0] ] assert FileQuery.parse('sort:created,-title').apply_sorting(data) == [ data[1], data[2], data[3], data[0] ]
def test_referent_skips_invalid_urls(): assert LinkInfo('foo', 'file://no[').referent() is None
def test_referent_self(): assert LinkInfo('/foo/bar', 'bar#baz').referent() == '/foo/bar' assert LinkInfo('/foo/bar', '#baz').referent() == '/foo/bar'
def test_referent_handles_special_characters(): assert LinkInfo('/foo', 'hi%20there%21').referent() == '/hi there!' assert LinkInfo('/foo', 'hi+there%21').referent() == '/hi there!'
def test_referent_resolves_relative_to_referrer(fs): fs.cwd = '/meh' assert LinkInfo('/foo/bar', 'baz').referent() == os.path.realpath('../foo/baz')
def test_referent_ignores_query_and_fragment(): assert LinkInfo('/foo', 'bar#baz').referent() == '/bar' assert LinkInfo('/foo', 'bar?baz').referent() == '/bar'
def test_referent_resolves_symlinks(fs): fs.cwd = '/cwd' fs.create_symlink('/cwd/bar', '/cwd/target') assert LinkInfo('foo', 'bar/baz').referent() == '/cwd/target/baz'
def test_referent_matches_relative_paths(): assert LinkInfo('/baz/foo', 'bar').referent() == '/baz/bar'
def test_referent_matches_absolute_paths(): assert LinkInfo('foo', '/bar').referent() == '/bar' assert LinkInfo('foo', 'file:///bar').referent() == '/bar' assert LinkInfo('foo', 'file://localhost/bar').referent() == '/bar'
def test_referent_skips_non_local_hosts(): assert LinkInfo('foo', 'file://example.com/bar').referent() is None
def test_referent_skips_non_file_schemes(): assert LinkInfo('foo', 'http:///bar').referent() is None
def test_referrers_self(fs): fs.create_file('/notes/subject.md', contents='[1](subject.md)') repo = DirectRepoConf(root_paths={'/notes'}).instantiate() info = repo.info('/notes/subject.md', 'backlinks') assert info.backlinks == [LinkInfo('/notes/subject.md', 'subject.md')]