diff --git a/purs/Makefile b/purs/Makefile
index 674cd18..a944dab 100644
--- a/purs/Makefile
+++ b/purs/Makefile
@@ -22,6 +22,8 @@ docs:
@rm -Rf generated-docs
@purs docs ".spago/*/*/src/**/*.purs" --format html
+docset: docs
+ @(cd docset; python3 ./gen-docset.py)
clean:
rm -f dist/*
diff --git a/purs/docset/Info.plist.in b/purs/docset/Info.plist.in
new file mode 100644
index 0000000..9ad9620
--- /dev/null
+++ b/purs/docset/Info.plist.in
@@ -0,0 +1,20 @@
+
+
+
+
+ CFBundleIdentifier
+ purescript-local
+ CFBundleName
+ purescript-local
+ DocSetPlatformFamily
+ purescript
+ dashIndexFilePath
+ index.html
+ isDashDocset
+
+ DashDocSetPlayURL
+ http://try.purescript.org
+ DashDocSetFamily
+ dashtoc
+
+
diff --git a/purs/docset/favicon-16x16.png b/purs/docset/favicon-16x16.png
new file mode 100644
index 0000000..27e79c7
Binary files /dev/null and b/purs/docset/favicon-16x16.png differ
diff --git a/purs/docset/favicon-32x32.png b/purs/docset/favicon-32x32.png
new file mode 100644
index 0000000..aa5ed0f
Binary files /dev/null and b/purs/docset/favicon-32x32.png differ
diff --git a/purs/docset/gen-docset.py b/purs/docset/gen-docset.py
new file mode 100755
index 0000000..e2c9a91
--- /dev/null
+++ b/purs/docset/gen-docset.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+
+import re
+import sys
+import os
+import sqlite3
+import urllib.parse
+import requests
+import shutil
+from shutil import copyfile
+from html import unescape
+from bs4 import BeautifulSoup
+
+def fatal(msg):
+ print(msg, file=sys.stderr)
+ sys.exit(1)
+
+class URLUtilities:
+ HTML = '../generated-docs/'
+ INDEX = HTML + 'index.html'
+
+class HTMLUtilities:
+ @staticmethod
+ def find_modules(html):
+ return re.findall(r'
([^<]+)', html)
+
+class Generator:
+ OUTPUT = 'purescript-local.docset'
+
+ def __init__(self):
+ self.version = None
+
+ def generate(self):
+ self.create_docset()
+ self.create_index()
+ self.save_assets()
+ modules = self.fetch_index()
+ print('Processing {} modules'.format(len(modules)))
+ for module in modules:
+ self.fetch_module(module)
+ self.db.close()
+ self.create_plist()
+ print('Done')
+
+ def fetch_index(self):
+ self.version = None
+ print('Processing module list')
+ with open(URLUtilities.INDEX, 'r') as f:
+ r = f.read()
+ html = re.sub(r'()', r'\1', r) # fix html error
+ self.save_html(html, self.documents_path('index.html'))
+ modules = HTMLUtilities.find_modules(html)
+ return modules
+
+ @staticmethod
+ def create_docset():
+ path = Generator.OUTPUT
+ if os.path.exists(path):
+ shutil.rmtree(path)
+ os.makedirs(Generator.documents_path())
+
+ def fetch_module(self, module):
+ print('Processing module {}'.format(module))
+ moduleFile = urllib.parse.quote(module, '') + '.html'
+ with open('{}/{}'.format(URLUtilities.HTML, moduleFile), 'r') as f:
+ r = f.read()
+ html = self.save_html(r, self.documents_path(moduleFile))
+ self.cursor.execute(
+ 'INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (?,?,?);',
+ [module, 'Module', moduleFile])
+ self.db.commit()
+ return r
+
+ @staticmethod
+ def documents_path(*paths):
+ return os.path.join(Generator.OUTPUT, 'Contents/Resources/Documents', *paths)
+
+ def save_html(self, html, path):
+ prefix = r''
+ soup = BeautifulSoup(html, 'html.parser')
+ # remove google font
+ soup.find('link', href=re.compile(r'^https://fonts\.googleapis\.com/.*')).decompose()
+ # remove top banner
+ soup.find('div', class_='top-banner').decompose()
+ aside = soup.find('div', class_='col--aside')
+ if(aside):
+ aside.decompose()
+ # find anchors
+ tlds = soup.find_all('div', class_='decl')
+ for tld in tlds:
+ self.process_decl(path, tld, soup)
+ with open(path, 'w') as f:
+ f.write(str(soup))
+
+ def process_decl(self, path, decl, soup, type_hint = None):
+ type_, name = decl.get('id').split(':', 1)
+ name = unescape(name)
+ if type_hint:
+ type_ = type_hint
+ else:
+ type_ = self.convert_type(type_)
+ signature = decl.find('pre', class_='decl__signature')
+ if signature:
+ if signature.code.find() == signature.code.find('span', class_='keyword', text='class'):
+ type_ = 'Class'
+ anchor_toc = '//apple_ref/cpp/{}/{}'.format(urllib.parse.quote(type_, ''), urllib.parse.quote(name, ''))
+ self.cursor.execute(
+ 'INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (?,?,?);',
+ [name, type_, '{}#{}'.format(os.path.relpath(path, self.documents_path()), anchor_toc)])
+ a = soup.new_tag('a', attrs={ 'name': anchor_toc, 'class': 'dashAnchor' })
+ decl.insert(0, a)
+ if type_ == 'Class':
+ members_lbl = decl.find('h4', text='Members')
+ if members_lbl:
+ for member in members_lbl.find_next_sibling().find_all('li', recursive=False):
+ self.process_decl(path, member, soup, 'Function')
+ elif type_ == 'Type':
+ ctors_lbl = decl.find('h4', text='Constructors')
+ if ctors_lbl:
+ for ctor in ctors_lbl.find_next_sibling().find_all('li', recursive=False):
+ self.process_decl(path, ctor, soup, 'Constructor')
+
+ def save_assets(self):
+ copyfile('favicon-16x16.png', self.documents_path('../../../icon.png'))
+ copyfile('favicon-32x32.png', self.documents_path('../../../icon@2x.png'))
+
+ @staticmethod
+ def create_plist():
+ with open('Info.plist.in', 'r') as f:
+ plist = f.read()
+ with open(Generator.documents_path('../../Info.plist'), 'w') as f:
+ f.write(plist)
+
+ @staticmethod
+ def convert_type(t):
+ TABLE = {
+ 't': 'Type',
+ 'v': 'Function',
+ 'k': 'Kind',
+ }
+ return TABLE[t] if t in TABLE else t
+
+ def create_index(self):
+ self.db = sqlite3.connect(self.documents_path('../docSet.dsidx'))
+ self.cursor = self.db.cursor()
+ self.cursor.execute('CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT);')
+ self.cursor.execute('CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path);')
+
+if __name__ == '__main__':
+ gen = Generator()
+ gen.generate()