~cypheon/ocaml-docset

75f12301676bf1925c56694b51bb9c2afa281a33 — Johann Rudloff 4 years ago d712ce4
Finish up for release.
7 files changed, 186 insertions(+), 35 deletions(-)

A .gitignore
M Info.plist
A LICENSE-OCaml.md
A LICENSE.md
M Makefile
A compare.py
M mkindex.py
A .gitignore => .gitignore +2 -0
@@ 0,0 1,2 @@
/files/
/target/

M Info.plist => Info.plist +14 -10
@@ 1,14 1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleIdentifier</key>
	<string>ocaml-unofficial</string>
	<key>CFBundleName</key>
	<string>OCaml (Unofficial)</string>
	<key>DocSetPlatformFamily</key>
	<string>ocaml-unofficial</string>
	<key>isDashDocset</key>
	<true/>
</dict>
  <dict>
    <key>CFBundleIdentifier</key>
    <string>ocaml-unofficial</string>
    <key>CFBundleName</key>
    <string>OCaml (Unofficial)</string>
    <key>DocSetPlatformFamily</key>
    <string>ocaml</string>
    <key>isDashDocset</key>
    <true/>
    <key>dashIndexFilePath</key>
    <string>htmlman/index.html</string>
    <key>DashDocSetFamily</key>
    <string>dashtoc</string>
  </dict>
</plist>

A LICENSE-OCaml.md => LICENSE-OCaml.md +14 -0
@@ 0,0 1,14 @@
# License (OCaml documentation and user’s manual)

The OCaml system is copyright © 1996–2019 Institut National de Recherche en Informatique et en Automatique (INRIA). INRIA holds all ownership rights to the OCaml system.

The OCaml system is open source and can be freely redistributed. See the file `LICENSE` in the distribution for licensing information.

The present documentation is copyright © 2019 Institut National de Recherche en Informatique et en Automatique (INRIA). The OCaml documentation and user’s manual may be reproduced and distributed in whole or in part, subject to the following conditions:

 *  The copyright notice above and this permission notice must be preserved complete on all complete or partial copies.
 *  Any translation or derivative work of the OCaml documentation and user’s manual must be approved by the authors in writing before distribution.
 *  If you distribute the OCaml documentation and user’s manual in part, instructions for obtaining the complete version of this manual must be included, and a means for obtaining a complete version provided.
 *  Small portions may be reproduced as illustrations for reviews or quotes in other works without this permission notice if proper citation is given. 



A LICENSE.md => LICENSE.md +13 -0
@@ 0,0 1,13 @@
# License (Docset Generation Script)

For the license of the original OCaml documentation and user's manual, please see `LICENSE-OCaml.md`.

Copyright 2019 Johann Rudloff

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

M Makefile => Makefile +38 -10
@@ 1,18 1,46 @@
ROOT=ocaml-unofficial.docset
CONTENTS=$(ROOT)/Contents/Resources/Documents
ORIGINAL_DOC=files/ocaml-4.09-refman-html.tar.gz
BUILD=_build
TARGET = target
DOCSET_NAME = ocaml-unofficial
ORIGONAL_DOC_URL = https://caml.inria.fr/distrib/ocaml-4.09/ocaml-4.09-refman-html.tar.gz

all: extract copy
ORIGINAL_DOC = files/ocaml-4.09-refman-html.tar.gz
ROOT = $(TARGET)/$(DOCSET_NAME).docset
RESOURCES = $(ROOT)/Contents/Resources
CONTENTS = $(RESOURCES)/Documents

all: docset
docset: mkindex extra-files

$(CONTENTS):
	mkdir -p $@

extract:
	mkdir -p $(BUILD)/source
	tar xf $(ORIGINAL_DOC) -C $(BUILD)/source
download: $(ORIGINAL_DOC)

$(ORIGINAL_DOC):
	mkdir -p files
	curl -L -o "$@" "$(ORIGONAL_DOC_URL)"

extract: $(ORIGINAL_DOC)
	mkdir -p $(TARGET)/source
	tar xf $(ORIGINAL_DOC) -C $(TARGET)/source

copy: extract $(CONTENTS)
	cp -av $(BUILD)/source/htmlman/. $(CONTENTS)
	mkdir -p $(CONTENTS)
	cp -a $(TARGET)/source/htmlman $(CONTENTS)

mkindex: copy
	pipenv run ./mkindex.py $(TARGET)/source $(RESOURCES)

extra-files:
	cp Info.plist $(ROOT)/Contents/

clean-target:
	rm -rf $(TARGET)

clean: clean-target
	@echo "Removing only generated files"
	@echo "Run 'make clean-all' to remove downloaded files as well."

clean-all: clean-target
	rm -rf files

.PHONY: extract
.PHONY: all clean clean-all clean-target copy docset download extra-files extract mkindex

A compare.py => compare.py +33 -0
@@ 0,0 1,33 @@
#!/usr/bin/env python3

import sqlite3

def getall(filename):
    db = sqlite3.connect(f'file:{filename}?mode=ro', uri=True)
    c = db.cursor()
    c.execute('SELECT name, type, path FROM searchIndex')
    result = c.fetchall()
    db.close()
    return result


def run(orig_fn, new_fn):
    orig_rows = getall(orig_fn)
    new_rows = getall(new_fn)

    ref = {}

    for row in orig_rows:
        ref[row[0]] = row[1:]

    orig_names = set(x[0] for x in orig_rows)
    new_names = set(x[0] for x in new_rows)

    missing = orig_names - new_names

    for row in sorted(missing):
        print(f'missing: {row:15s} -> {ref[row]}')

if __name__ == '__main__':
    import sys
    run(sys.argv[1], sys.argv[2])

M mkindex.py => mkindex.py +72 -15
@@ 2,6 2,8 @@

import os
import re
import sqlite3
import urllib.parse

from bs4 import BeautifulSoup



@@ 14,10 16,14 @@ TYPE_MODULE      = 'Module'
TYPE_TYPE        = 'Type'
TYPE_VALUE       = 'Value'

RE_LIBRARY_CHAPTER = re.compile(r'.+The ([^ ]+) library')
RE_LIBRARY_CHAPTER = re.compile(r'.+The ([^ ]+) library(?:|: .+)')

def add_index(name, typ, path):
    print(f'{name:32s}  {typ:12s}  {path}')
    c = conn.cursor()
    c.execute('''INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (?, ?, ?)''',
              (name, typ, path))
    conn.commit()
    # print(f'{name:32s}  {typ:12s}  {path}')

def contains(node, string):
    for s in node.strings:


@@ 25,29 31,42 @@ def contains(node, string):
            return True
    return False

def run(filename):
    with open(filename) as fp:
def run(filename, file_path):
    with open(file_path) as fp:
        soup = BeautifulSoup(fp, 'html.parser')
    soup.made_changes = False
    h1 = soup.find('h1')
    if h1 is None:
        print('WARN: no h1: ' + filename)
        return
        if not os.path.basename(filename).startswith('type_'):
            print('WARN: no h1: ' + filename)
        return soup, []
    h1_content = list(h1.stripped_strings)
    libmatch = RE_LIBRARY_CHAPTER.fullmatch(' '.join(h1_content))
    def anchor(id):
        return filename + '#' + id
    if h1_content[0].startswith('Module'):

    if h1_content[0].startswith('Module') or h1_content[0].startswith('Functor'):
        module_name = h1_content[1]
        add_index(module_name, TYPE_MODULE, filename)
        handle_module(filename, module_name, soup)
        return soup, []
    elif libmatch is not None:
        libname = libmatch.group(1)
        add_index(libname, TYPE_LIBRARY, anchor(h1['id']))
        handle_library(filename, libname, soup)
        return soup, []
    else:
        print('WARN: no module: ' + filename)
        return
        if not os.path.basename(filename).startswith('index_'):
            print('WARN: no module: ' + filename)
        return soup, []

def anchor_element(soup, typ, id):
    id_quoted = urllib.parse.quote(id, safe='')
    a = soup.new_tag('a')
    a.attrs['name'] = f'//apple_ref/cpp/{typ}/{id_quoted}'
    a.attrs['class'] = 'dashAnchor'
    soup.made_changes = True
    return a

RE_LIB_TYPE = re.compile(r'type (?:.+ |)([a-zA-Z_][a-zA-Z0-9_]*)')
RE_LIB_EXN = re.compile(r'exception ([a-zA-Z_][a-zA-Z0-9_]*)(?: of .+|)')


@@ 63,6 82,7 @@ def handle_library(filename, library_name, soup):
    def getid(element):
        if 'id' not in element.attrs:
            element['id'] = autoid()
            soup.made_changes = True
        return element['id']

    for pre in soup.find_all('pre'):


@@ 71,12 91,14 @@ def handle_library(filename, library_name, soup):
        if m_type is not None:
            typname = m_type.group(1)
            add_index(typname, TYPE_TYPE, anchor(getid(pre)))
            pre.insert_before(anchor_element(soup, TYPE_TYPE, typname))
            continue

        m_exn = RE_LIB_EXN.fullmatch(pretext)
        if m_exn is not None:
            exnname = m_exn.group(1)
            add_index(exnname, TYPE_EXCEPTION, anchor(getid(pre)))
            pre.insert_before(anchor_element(soup, TYPE_EXCEPTION, exnname))
            continue

def handle_module(filename, module_name, soup):


@@ 88,17 110,23 @@ def handle_module(filename, module_name, soup):
        if spanid.startswith('TYPEELT'):
            name = spanid[7:]
            # this can either be a constructor or a record field
            full_code = ' '.join(span.parent.stripped_strings)
            if ':' in full_code:
                add_index(f'{module_name}.{name}', TYPE_FIELD, anchor(spanid))
            # full_code = ' '.join(span.parent.stripped_strings)
            if name.split('.')[-1][0].islower():
                typ = TYPE_FIELD
            else:
                add_index(f'{module_name}.{name}', TYPE_CONSTRUCTOR, anchor(spanid))
                typ = TYPE_CONSTRUCTOR
            add_index(f'{module_name}.{name}', typ, anchor(spanid))
            span.parent.insert_before(anchor_element(soup, typ, name))

        elif spanid.startswith('TYPE'):
            name = spanid[4:]
            span.parent.insert_before(anchor_element(soup, TYPE_TYPE, name))
            add_index(f'{module_name}.{name}', TYPE_TYPE, anchor(spanid))
            # add_index(f'{module_name}.{name}', TYPE_TYPE, anchor(f'//apple_ref/cpp/{TYPE_TYPE}/{name}'))
        elif spanid.startswith('EXCEPTION'):
            name = spanid[9:]
            add_index(f'{module_name}.{name}', TYPE_EXCEPTION, anchor(spanid))
            span.parent.insert_before(anchor_element(soup, TYPE_EXCEPTION, name))
        elif spanid.startswith('VAL'):
            name = spanid[3:]
            if contains(span.parent, '->'):


@@ 106,13 134,42 @@ def handle_module(filename, module_name, soup):
            else:
                valtype = TYPE_VALUE
            add_index(f'{module_name}.{name}', valtype, anchor(spanid))
            span.parent.insert_before(anchor_element(soup, valtype, name))
            # print(list(span.parent.strings))

if __name__ == '__main__':
    import glob
    import shutil
    import sys
    import traceback
    for filename in sys.argv[1:]:

    input_dir = sys.argv[1]
    output_dir = sys.argv[2]
    files = glob.glob(input_dir + '/**/*.html', recursive=True)

    db_filename = os.path.join(output_dir, 'docSet.dsidx')

    if os.path.isfile(db_filename):
        os.unlink(db_filename)
    conn = sqlite3.connect(db_filename)
    c = conn.cursor()
    c.execute('''CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT)''')
    c.execute('''CREATE UNIQUE INDEX anchor ON searchIndex (name, type, path)''')
    conn.commit()

    for filename in files:
        relname = os.path.relpath(filename, start=input_dir)
        try:
            run(filename)
            output_filename = os.path.join(output_dir, 'Documents', relname)
            if not os.path.isdir(os.path.dirname(output_filename)):
                os.makedirs(os.path.dirname(output_filename))
            doc, entries = run(relname, filename)
            if doc is not None and doc.made_changes:
                with open(output_filename, 'w') as f:
                    f.write(str(doc))
            else:
                # No need to copy, this has already been taken care of by make
                # shutil.copy(filename, output_filename)
                pass
        except:
            traceback.print_exc()