Esempio n. 1
0
def clean(path_book, all_, html, latex):
    """Empty the _build directory except jupyter_cache.
    If the all option has been flagged, it will remove the entire _build. If html/latex
    option is flagged, it will remove the html/latex subdirectories."""
    def remove_option(path, option, rm_both=False):
        """Remove folder specified under option. If rm_both is True, remove folder and
        skip message_box."""
        option_path = path.joinpath(option)
        if not option_path.is_dir():
            return

        sh.rmtree(option_path)
        if not rm_both:
            _message_box(f"Your {option} directory has been removed")

    def remove_html_latex(path):
        """Remove both html and latex folders."""
        print_msg = False
        for opt in ["html", "latex"]:
            if path.joinpath(opt).is_dir():
                print_msg = True
            remove_option(path, opt, True)

        if print_msg:
            _message_box("Your html and latex directories have been removed")

    def remove_all(path):
        """Remove _build directory entirely."""
        sh.rmtree(path)
        _message_box("Your _build directory has been removed")

    def remove_default(path):
        """Remove all subfolders in _build except .jupyter_cache."""
        to_remove = [
            dd for dd in path.iterdir()
            if dd.is_dir() and dd.name != ".jupyter_cache"
        ]
        for dd in to_remove:
            sh.rmtree(path.joinpath(dd.name))
        _message_box(
            "Your _build directory has been emptied except for .jupyter_cache")

    PATH_OUTPUT = Path(path_book).absolute()
    if not PATH_OUTPUT.is_dir():
        _error(f"Path to book isn't a directory: {PATH_OUTPUT}")

    build_path = PATH_OUTPUT.joinpath("_build")
    if not build_path.is_dir():
        return

    if all_:
        remove_all(build_path)
    elif html and latex:
        remove_html_latex(build_path)
    elif html:
        remove_option(build_path, "html")
    elif latex:
        remove_option(build_path, "latex")
    else:
        remove_default(build_path)
Esempio n. 2
0
def create(path_book, cookiecutter):
    """Create a Jupyter Book template that you can customize."""
    book = Path(path_book)
    if not cookiecutter:  # this will be the more common option
        template_path = Path(__file__).parent.parent.joinpath("book_template")
        sh.copytree(template_path, book)
    else:
        cc_url = "gh:executablebooks/cookiecutter-jupyter-book"
        try:
            from cookiecutter.main import cookiecutter
        except ModuleNotFoundError as e:
            _error(
                f"{e}. To install, run\n\n\tpip install cookiecutter",
                kind=e.__class__,
            )
        book = cookiecutter(cc_url, output_dir=Path(path_book))
    _message_box(f"Your book template can be found at\n\n    {book}{os.sep}")
Esempio n. 3
0
def build_book(path_book,
               path_toc_yaml=None,
               config_file=None,
               path_template=None,
               local_build=False,
               execute=False,
               overwrite=False):
    """Build the markdown for a book using its TOC and a content folder.

    Parameters
    ----------
    path_book : str
        Path to the root of the book repository
    path_toc_yaml : str | None
        Path to the Table of Contents YAML file
    config_file : str | None
        Path to the Jekyll configuration file
    path_template : str | None
        Path to the template nbconvert uses to build markdown
        files
    local_build : bool
        Specify you are building site locally for later upload
    execute : bool
        Whether to execute notebooks before converting to markdown
    overwrite : bool
        Whether to overwrite existing markdown files
    """

    PATH_IMAGES_FOLDER = op.join(path_book, '_build', 'images')
    BUILD_FOLDER = op.join(path_book, BUILD_FOLDER_NAME)

    ###############################################
    # Read in textbook configuration

    # Load the yaml for this site
    with open(config_file, 'r') as ff:
        site_yaml = yaml.safe_load(ff.read())
    CONTENT_FOLDER_NAME = site_yaml.get('content_folder_name').strip('/')
    PATH_CONTENT_FOLDER = op.join(path_book, CONTENT_FOLDER_NAME)

    # Load the textbook yaml for this site
    if not op.exists(path_toc_yaml):
        raise _error("No toc.yml file found, please create one at `{}`".format(
            path_toc_yaml))
    with open(path_toc_yaml, 'r') as ff:
        toc = yaml.safe_load(ff.read())

    # Drop divider items and non-linked pages in the sidebar, un-nest sections
    toc = _prepare_toc(toc)

    ################################################
    # Generating the Jekyll files for all content

    n_skipped_files = 0
    n_built_files = 0
    case_check = _case_sensitive_fs(BUILD_FOLDER) and local_build
    print("Convert and copy notebook/md files...")
    for ix_file, page in enumerate(tqdm(list(toc))):
        url_page = page.get('url', None)
        title = page.get('title', None)
        if page.get('external', None):
            # If its an external link, just pass
            continue

        # Make sure URLs (file paths) have correct structure
        _check_url_page(url_page, CONTENT_FOLDER_NAME)

        ##############################################
        # Create path to old/new file and create directory

        # URL will be relative to the CONTENT_FOLDER
        path_url_page = os.path.join(PATH_CONTENT_FOLDER, url_page.lstrip('/'))
        path_url_folder = os.path.dirname(path_url_page)

        # URLs shouldn't have the suffix in there already so
        # now we find which one to add
        for suf in SUPPORTED_FILE_SUFFIXES:
            if op.exists(path_url_page + suf):
                path_url_page = path_url_page + suf
                break

        if not op.exists(path_url_page):
            raise _error(
                "Could not find file called {} with any of these extensions: {}"
                .format(path_url_page, SUPPORTED_FILE_SUFFIXES))

        # Create and check new folder / file paths
        path_build_new_folder = path_url_folder.replace(
            os.sep + CONTENT_FOLDER_NAME, os.sep + BUILD_FOLDER_NAME) + os.sep
        path_build_new_file = op.join(
            path_build_new_folder,
            op.basename(path_url_page).replace('.ipynb', '.md'))

        if overwrite is False and op.exists(path_build_new_file) \
           and os.stat(path_build_new_file).st_mtime > os.stat(path_url_page).st_mtime:
            n_skipped_files += 1
            continue

        if not op.isdir(path_build_new_folder):
            os.makedirs(path_build_new_folder)

        ################################################
        # Generate previous/next page URLs
        if ix_file == 0:
            url_prev_page = ''
            prev_file_title = ''
        else:
            prev_file_title = toc[ix_file - 1].get('title')
            url_prev_page = toc[ix_file - 1].get('url')
            pre_external = toc[ix_file - 1].get('external', False)
            if pre_external is False:
                url_prev_page = _prepare_url(url_prev_page)

        if ix_file == len(toc) - 1:
            url_next_page = ''
            next_file_title = ''
        else:
            next_file_title = toc[ix_file + 1].get('title')
            url_next_page = toc[ix_file + 1].get('url')
            next_external = toc[ix_file + 1].get('external', False)
            if next_external is False:
                url_next_page = _prepare_url(url_next_page)

        ###############################################################################
        # Get kernel name and presence of widgets from notebooks metadata

        kernel_name = ''
        if path_url_page.endswith('.ipynb'):
            data = nbf.read(path_url_page, nbf.NO_CONVERT)
            if 'metadata' in data and 'kernelspec' in data['metadata']:
                kernel_name = data['metadata']['kernelspec']['name']
            has_widgets = "true" if any(
                "interactive" in cell['metadata'].get('tags', [])
                for cell in data['cells']) else "false"

        ############################################
        # Content conversion

        # Convert notebooks or just copy md if no notebook.
        if path_url_page.endswith('.ipynb'):
            notebook_name = op.splitext(op.basename(path_url_page))[0]
            ntbk = nbf.read(path_url_page, nbf.NO_CONVERT)

            ########################################
            # Notebook cleaning

            # Clean up the file before converting
            cleaner = NotebookCleaner(ntbk)
            cleaner.remove_cells(empty=True)
            cleaner.clear('stderr')
            ntbk = cleaner.ntbk
            _clean_notebook_cells(ntbk)

            #############################################
            # Conversion to Jekyll Markdown
            # create a configuration object that changes the preprocessors
            c = Config()
            c.FilesWriter.build_directory = path_build_new_folder

            if execute is True:
                # Excution of the notebook if we wish
                ep = ExecutePreprocessor(timeout=600, kernel_name=kernel_name)
                ep.preprocess(
                    ntbk, {'metadata': {
                        'path': op.dirname(path_url_folder)
                    }})

            # Define the path to images and then the relative path to where they'll originally be placed
            path_after_build_folder = path_build_new_folder.split(
                os.sep + BUILD_FOLDER_NAME + os.sep)[-1]
            path_images_new_folder = op.join(PATH_IMAGES_FOLDER,
                                             path_after_build_folder)
            path_images_rel = op.relpath(path_images_new_folder,
                                         path_build_new_folder)

            # Generate Markdown from our notebook using the template
            output_resources = {
                'output_files_dir': path_images_rel,
                'unique_key': notebook_name
            }
            exp = MarkdownExporter(template_file=path_template, config=c)
            markdown, resources = exp.from_notebook_node(
                ntbk, resources=output_resources)

            # Now write the markdown and resources
            writer = FilesWriter(config=c)
            writer.write(markdown, resources, notebook_name=notebook_name)

        elif path_url_page.endswith('.md'):
            # If a non-notebook file, just copy it over.
            # If markdown we'll add frontmatter later
            sh.copy2(path_url_page, path_build_new_file)
        else:
            raise _error("Files must end in ipynb or md. Found file {}".format(
                path_url_page))

        ###############################################################################
        # Modify the generated Markdown to work with Jekyll
        # Clean markdown for Jekyll quirks (e.g. extra escape characters)
        with open(path_build_new_file, 'r', encoding='utf8') as ff:
            lines = ff.readlines()
        lines = _clean_lines(lines, path_build_new_file, path_book,
                             PATH_IMAGES_FOLDER)

        # Split off original yaml
        yaml_orig, lines = _split_yaml(lines)

        # Front-matter YAML
        yaml_fm = []
        yaml_fm += ['---']
        # In case pre-existing links are sanitized
        sanitized = url_page.lower().replace('_', '-')
        if sanitized != url_page:
            if case_check and url_page.lower() == sanitized:
                raise RuntimeError(
                    'Redirect {} clashes with page {} for local build on '
                    'case-insensitive FS\n'.format(sanitized, url_page) +
                    'Rename source page to lower case or build on a case '
                    'sensitive FS, e.g. case-sensitive disk image on Mac')
            yaml_fm += ['redirect_from:']
            yaml_fm += ['  - "{}"'.format(sanitized)]
        if path_url_page.endswith('.ipynb'):
            interact_path = CONTENT_FOLDER_NAME + '/' + \
                path_url_page.split(CONTENT_FOLDER_NAME + '/')[-1]
            yaml_fm += ['interact_link: {}'.format(interact_path)]
            yaml_fm += ["kernel_name: {}".format(kernel_name)]
            yaml_fm += ["has_widgets: {}".format(has_widgets)]

        # Page metadata
        yaml_fm += ["title: '{}'".format(title)]
        yaml_fm += ['prev_page:']
        yaml_fm += ['  url: {}'.format(url_prev_page)]
        yaml_fm += ["  title: '{}'".format(prev_file_title)]
        yaml_fm += ['next_page:']
        yaml_fm += ['  url: {}'.format(url_next_page)]
        yaml_fm += ["  title: '{}'".format(next_file_title)]

        # Add back any original YaML, and end markers
        yaml_fm += yaml_orig
        yaml_fm += [
            'comment: "***PROGRAMMATICALLY GENERATED, DO NOT EDIT. SEE ORIGINAL FILES IN /{}***"'
            .format(CONTENT_FOLDER_NAME)
        ]
        yaml_fm += ['---']
        yaml_fm = [ii + '\n' for ii in yaml_fm]
        lines = yaml_fm + lines

        # Write the result as UTF-8.
        with open(path_build_new_file, 'w', encoding='utf8') as ff:
            ff.writelines(lines)
        n_built_files += 1

    #######################################################
    # Finishing up...

    # Copy non-markdown files in notebooks/ in case they're referenced in the notebooks
    print('Copying non-content files inside `{}/`...'.format(
        CONTENT_FOLDER_NAME))
    _copy_non_content_files(PATH_CONTENT_FOLDER, CONTENT_FOLDER_NAME,
                            BUILD_FOLDER_NAME)

    # Message at the end
    msg = [
        "Generated {} new files\nSkipped {} already-built files".format(
            n_built_files, n_skipped_files)
    ]
    if n_built_files == 0:
        msg += [
            "Delete the markdown files in '{}' for any pages that you wish to re-build, or use --overwrite option to re-build all."
            .format(BUILD_FOLDER_NAME)
        ]
    msg += ["Your Jupyter Book is now in `{}/`.".format(BUILD_FOLDER_NAME)]
    msg += ["Demo your Jupyter book with `make serve` or push to GitHub!"]
    print_message_box('\n'.join(msg))
Esempio n. 4
0
def build_book():
    """Build the markdown for a book using its TOC and a content folder."""
    parser = argparse.ArgumentParser(description=DESCRIPTION)
    parser.add_argument("path_book",
                        help="Path to the root of the book repository.")
    parser.add_argument(
        "--template",
        default=None,
        help="Path to the template nbconvert uses to build markdown files")
    parser.add_argument("--config",
                        default=None,
                        help="Path to the Jekyll configuration file")
    parser.add_argument("--toc",
                        default=None,
                        help="Path to the Table of Contents YAML file")
    parser.add_argument("--overwrite",
                        action='store_true',
                        help="Overwrite md files if they already exist.")
    parser.add_argument("--execute",
                        action='store_true',
                        help="Execute notebooks before converting to MD.")
    parser.add_argument(
        "--local-build",
        action='store_true',
        help="Specify you are building site locally for later upload.")
    parser.set_defaults(overwrite=False, execute=False)

    ###############################################################################
    # Default values and arguments

    args = parser.parse_args(sys.argv[2:])
    overwrite = bool(args.overwrite)
    execute = bool(args.execute)

    # Paths for our notebooks
    PATH_BOOK = op.abspath(args.path_book)

    PATH_TOC_YAML = args.toc if args.toc is not None else op.join(
        PATH_BOOK, '_data', 'toc.yml')
    CONFIG_FILE = args.config if args.config is not None else op.join(
        PATH_BOOK, '_config.yml')
    PATH_TEMPLATE = args.template if args.template is not None else op.join(
        PATH_BOOK, 'scripts', 'templates', 'jekyllmd.tpl')
    PATH_IMAGES_FOLDER = op.join(PATH_BOOK, '_build', 'images')
    BUILD_FOLDER = op.join(PATH_BOOK, BUILD_FOLDER_NAME)

    ###############################################################################
    # Read in textbook configuration

    # Load the yaml for this site
    with open(CONFIG_FILE, 'r') as ff:
        site_yaml = yaml.load(ff.read())
    CONTENT_FOLDER_NAME = site_yaml.get('content_folder_name').strip('/')
    PATH_CONTENT_FOLDER = op.join(PATH_BOOK, CONTENT_FOLDER_NAME)

    # Load the textbook yaml for this site
    if not op.exists(PATH_TOC_YAML):
        raise _error("No toc.yml file found, please create one at `{}`".format(
            PATH_TOC_YAML))
    with open(PATH_TOC_YAML, 'r') as ff:
        toc = yaml.load(ff.read())

    # Drop divider items and non-linked pages in the sidebar, un-nest sections
    toc = _prepare_toc(toc)

    ###############################################################################
    # Generating the Jekyll files for all content

    n_skipped_files = 0
    n_built_files = 0
    case_check = _case_sensitive_fs(BUILD_FOLDER) and args.local_build
    print("Convert and copy notebook/md files...")
    for ix_file, page in enumerate(tqdm(list(toc))):
        url_page = page.get('url', None)
        title = page.get('title', None)
        if page.get('external', None):
            # If its an external link, just pass
            continue

        # Make sure URLs (file paths) have correct structure
        _check_url_page(url_page, CONTENT_FOLDER_NAME)

        ###############################################################################
        # Create path to old/new file and create directory

        # URL will be relative to the CONTENT_FOLDER
        path_url_page = os.path.join(PATH_CONTENT_FOLDER, url_page.lstrip('/'))
        path_url_folder = os.path.dirname(path_url_page)

        # URLs shouldn't have the suffix in there already so now we find which one to add
        for suf in SUPPORTED_FILE_SUFFIXES:
            if op.exists(path_url_page + suf):
                path_url_page = path_url_page + suf
                break

        if not op.exists(path_url_page):
            raise _error(
                "Could not find file called {} with any of these extensions: {}"
                .format(path_url_page, SUPPORTED_FILE_SUFFIXES))

        # Create and check new folder / file paths
        path_new_folder = path_url_folder.replace(os.sep + CONTENT_FOLDER_NAME,
                                                  os.sep + BUILD_FOLDER_NAME)
        path_new_file = op.join(
            path_new_folder,
            op.basename(path_url_page).replace('.ipynb', '.md'))

        if overwrite is False and op.exists(path_new_file) \
           and os.stat(path_new_file).st_mtime > os.stat(path_url_page).st_mtime:
            n_skipped_files += 1
            continue

        if not op.isdir(path_new_folder):
            os.makedirs(path_new_folder)

        ###############################################################################
        # Generate previous/next page URLs
        if ix_file == 0:
            url_prev_page = ''
            prev_file_title = ''
        else:
            prev_file_title = toc[ix_file - 1].get('title')
            url_prev_page = toc[ix_file - 1].get('url')
            url_prev_page = _prepare_url(url_prev_page)

        if ix_file == len(toc) - 1:
            url_next_page = ''
            next_file_title = ''
        else:
            next_file_title = toc[ix_file + 1].get('title')
            url_next_page = toc[ix_file + 1].get('url')
            url_next_page = _prepare_url(url_next_page)

        ###############################################################################
        # Get kernel name from notebooks metadata

        kernel_name = ''
        if path_url_page.endswith('.ipynb'):
            data = nbf.read(path_url_page, nbf.NO_CONVERT)
            kernel_name = data['metadata']['kernelspec']['name']

        ###############################################################################
        # Content conversion

        # Convert notebooks or just copy md if no notebook.
        if path_url_page.endswith('.ipynb'):
            # Create a temporary version of the notebook we can modify
            tmp_notebook = path_url_page + '_TMP'
            sh.copy2(path_url_page, tmp_notebook)

            ###############################################################################
            # Notebook cleaning

            # Clean up the file before converting
            cleaner = NotebookCleaner(tmp_notebook)
            cleaner.remove_cells(empty=True)
            if site_yaml.get('hide_cell_text', False):
                cleaner.remove_cells(
                    search_text=site_yaml.get('hide_cell_text'))
            if site_yaml.get('hide_code_text', False):
                cleaner.clear(kind="content",
                              search_text=site_yaml.get('hide_code_text'))
            cleaner.clear('stderr')
            cleaner.save(tmp_notebook)
            _clean_notebook_cells(tmp_notebook)

            ###############################################################################
            # Conversion to Jekyll Markdown

            # Run nbconvert moving it to the output folder
            # This is the output directory for `.md` files
            build_call = '--FilesWriter.build_directory={}'.format(
                path_new_folder)
            # Copy notebook output images to the build directory using the base folder name
            path_after_build_folder = path_new_folder.split(os.sep +
                                                            BUILD_FOLDER_NAME +
                                                            os.sep)[-1]
            nb_output_folder = op.join(PATH_IMAGES_FOLDER,
                                       path_after_build_folder)
            images_call = '--NbConvertApp.output_files_dir={}'.format(
                nb_output_folder)
            call = [
                'jupyter', 'nbconvert', '--log-level="CRITICAL"', '--to',
                'markdown', '--template', PATH_TEMPLATE, images_call,
                build_call, tmp_notebook
            ]
            if execute is True:
                call.insert(-1, '--execute')

            check_call(call)
            os.remove(tmp_notebook)
        elif path_url_page.endswith('.md'):
            # If a non-notebook file, just copy it over.
            # If markdown we'll add frontmatter later
            sh.copy2(path_url_page, path_new_file)
        else:
            raise _error("Files must end in ipynb or md. Found file {}".format(
                path_url_page))

        ###############################################################################
        # Modify the generated Markdown to work with Jekyll

        # Clean markdown for Jekyll quirks (e.g. extra escape characters)
        with open(path_new_file, 'r') as ff:
            lines = ff.readlines()
        lines = _clean_lines(lines, path_new_file, PATH_BOOK,
                             PATH_IMAGES_FOLDER)

        # Split off original yaml
        yaml_orig, lines = _split_yaml(lines)

        # Front-matter YAML
        yaml_fm = []
        yaml_fm += ['---']
        # In case pre-existing links are sanitized
        sanitized = url_page.lower().replace('_', '-')
        if sanitized != url_page:
            if case_check and url_page.lower() == sanitized:
                raise RuntimeError(
                    'Redirect {} clashes with page {} for local build on '
                    'case-insensitive FS\n'.format(sanitized, url_page) +
                    'Rename source page to lower case or build on a case '
                    'sensitive FS, e.g. case-sensitive disk image on Mac')
            yaml_fm += ['redirect_from:']
            yaml_fm += ['  - "{}"'.format(sanitized)]
        if path_url_page.endswith('.ipynb'):
            interact_path = CONTENT_FOLDER_NAME + '/' + path_url_page.split(
                CONTENT_FOLDER_NAME + '/')[-1]
            yaml_fm += ['interact_link: {}'.format(interact_path)]
            yaml_fm += ["kernel_name: {}".format(kernel_name)]
        yaml_fm += ["title: '{}'".format(title)]
        yaml_fm += ['prev_page:']
        yaml_fm += ['  url: {}'.format(url_prev_page)]
        yaml_fm += ["  title: '{}'".format(prev_file_title)]
        yaml_fm += ['next_page:']
        yaml_fm += ['  url: {}'.format(url_next_page)]
        yaml_fm += ["  title: '{}'".format(next_file_title)]

        # Add back any original YaML, and end markers
        yaml_fm += yaml_orig
        yaml_fm += [
            'comment: "***PROGRAMMATICALLY GENERATED, DO NOT EDIT. SEE ORIGINAL FILES IN /{}***"'
            .format(CONTENT_FOLDER_NAME)
        ]
        yaml_fm += ['---']
        yaml_fm = [ii + '\n' for ii in yaml_fm]
        lines = yaml_fm + lines

        # Write the result
        with open(path_new_file, 'w') as ff:
            ff.writelines(lines)
        n_built_files += 1

    ###############################################################################
    # Finishing up...

    # Copy non-markdown files in notebooks/ in case they're referenced in the notebooks
    print('Copying non-content files inside `{}/`...'.format(
        CONTENT_FOLDER_NAME))
    _copy_non_content_files(PATH_CONTENT_FOLDER, CONTENT_FOLDER_NAME,
                            BUILD_FOLDER_NAME)

    # Message at the end
    msg = [
        "Generated {} new files\nSkipped {} already-built files".format(
            n_built_files, n_skipped_files)
    ]
    if n_built_files == 0:
        msg += [
            "Delete the markdown files in '{}' for any pages that you wish to re-build, or use --overwrite option to re-build all."
            .format(BUILD_FOLDER_NAME)
        ]
    msg += ["Your Jupyter Book is now in `{}/`.".format(BUILD_FOLDER_NAME)]
    msg += ["Demo your Jupyter book with `make serve` or push to GitHub!"]
    print_message_box('\n'.join(msg))
Esempio n. 5
0
def builder_specific_actions(result,
                             builder,
                             output_path,
                             cmd_type,
                             page_name=None,
                             print_func=print):
    """Run post-sphinx-build actions.

    :param result: the result of the build execution; a status code or and exception
    """

    from sphinx.util.osutil import cd

    from jupyter_book.pdf import html_to_pdf
    from jupyter_book.sphinx import REDIRECT_TEXT

    if isinstance(result, Exception):
        msg = (f"There was an error in building your {cmd_type}. "
               "Look above for the cause.")
        # TODO ideally we probably only want the original traceback here
        raise RuntimeError(_message_box(msg, color="red",
                                        doprint=False)) from result
    elif result:
        msg = (
            f"Building your {cmd_type}, returns a non-zero exit code ({result}). "
            "Look above for the cause.")
        _message_box(msg, color="red", print_func=click.echo)
        sys.exit(result)

    # Builder-specific options
    if builder == "html":
        path_output_rel = Path(op.relpath(output_path, Path()))
        if cmd_type == "page":
            path_page = path_output_rel.joinpath(f"{page_name}.html")
            # Write an index file if it doesn't exist so we get redirects
            path_index = path_output_rel.joinpath("index.html")
            if not path_index.exists():
                path_index.write_text(
                    REDIRECT_TEXT.format(first_page=path_page.name))

            _message_box(
                dedent(f"""
                    Page build finished.
                        Your page folder is: {path_page.parent}{os.sep}
                        Open your page at: {path_page}
                    """))

        elif cmd_type == "book":
            path_output_rel = Path(op.relpath(output_path, Path()))
            path_index = path_output_rel.joinpath("index.html")
            _message_box(f"""\
            Finished generating HTML for {cmd_type}.
            Your book's HTML pages are here:
                {path_output_rel}{os.sep}
            You can look at your book by opening this file in a browser:
                {path_index}
            Or paste this line directly into your browser bar:
                file://{path_index.resolve()}\
            """)
    if builder == "pdfhtml":
        print_func(f"Finished generating HTML for {cmd_type}...")
        print_func(f"Converting {cmd_type} HTML into PDF...")
        path_pdf_output = output_path.parent.joinpath("pdf")
        path_pdf_output.mkdir(exist_ok=True)
        if cmd_type == "book":
            path_pdf_output = path_pdf_output.joinpath("book.pdf")
            html_to_pdf(output_path.joinpath("index.html"), path_pdf_output)
        elif cmd_type == "page":
            path_pdf_output = path_pdf_output.joinpath(page_name + ".pdf")
            html_to_pdf(output_path.joinpath(page_name + ".html"),
                        path_pdf_output)
        path_pdf_output_rel = Path(op.relpath(path_pdf_output, Path()))
        _message_box(f"""\
        Finished generating PDF via HTML for {cmd_type}. Your PDF is here:
            {path_pdf_output_rel}\
        """)
    if builder == "pdflatex":
        print_func(f"Finished generating latex for {cmd_type}...")
        print_func(f"Converting {cmd_type} latex into PDF...")
        # Convert to PDF via tex and template built Makefile and make.bat
        if sys.platform == "win32":
            makecmd = os.environ.get("MAKE", "make.bat")
        else:
            makecmd = os.environ.get("MAKE", "make")
        try:
            with cd(output_path):
                output = subprocess.run([makecmd, "all-pdf"])
                if output.returncode != 0:
                    _error("Error: Failed to build pdf")
                    return output.returncode
            _message_box(f"""\
            A PDF of your {cmd_type} can be found at:
                {output_path}
            """)
        except OSError:
            _error("Error: Failed to run: %s" % makecmd)
            return 1
Esempio n. 6
0
def build(
    path_source,
    path_output,
    config,
    toc,
    warningiserror,
    nitpick,
    keep_going,
    freshenv,
    builder,
    custom_builder,
    verbose,
    quiet,
    individualpages,
    get_config_only=False,
):
    """Convert your book's or page's content to HTML or a PDF."""
    from sphinx_external_toc.parsing import MalformedError, parse_toc_yaml

    from jupyter_book import __version__ as jbv
    from jupyter_book.sphinx import build_sphinx

    if not get_config_only:
        click.secho(f"Running Jupyter-Book v{jbv}", bold=True, fg="green")

    # Paths for the notebooks
    PATH_SRC_FOLDER = Path(path_source).absolute()

    config_overrides = {}
    use_external_toc = True
    found_config = find_config_path(PATH_SRC_FOLDER)
    BUILD_PATH = path_output if path_output is not None else found_config[0]

    # Set config for --individualpages option (pages, documents)
    if individualpages:
        if builder != "pdflatex":
            _error("""
                Specified option --individualpages only works with the
                following builders:

                pdflatex
                """)

    # Build Page
    if not PATH_SRC_FOLDER.is_dir():
        # it is a single file
        build_type = "page"
        use_external_toc = False
        subdir = None
        PATH_SRC = Path(path_source)
        PATH_SRC_FOLDER = PATH_SRC.parent.absolute()
        PAGE_NAME = PATH_SRC.with_suffix("").name

        # checking if the page is inside a sub directory
        # then changing the build_path accordingly
        if str(BUILD_PATH) in str(PATH_SRC_FOLDER):
            subdir = str(PATH_SRC_FOLDER.relative_to(BUILD_PATH))
        if subdir and subdir != ".":
            subdir = subdir.replace("/", "-")
            subdir = subdir + "-" + PAGE_NAME
            BUILD_PATH = Path(BUILD_PATH).joinpath("_build", "_page", subdir)
        else:
            BUILD_PATH = Path(BUILD_PATH).joinpath("_build", "_page",
                                                   PAGE_NAME)

        # Find all files that *aren't* the page we're building and exclude them
        to_exclude = [
            op.relpath(ifile, PATH_SRC_FOLDER)
            for ifile in iglob(str(PATH_SRC_FOLDER.joinpath("**", "*")),
                               recursive=True)
            if ifile != str(PATH_SRC.absolute())
        ]
        to_exclude.extend(
            ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"])

        # Now call the Sphinx commands to build
        config_overrides = {
            "master_doc": PAGE_NAME,
            "exclude_patterns": to_exclude,
            "html_theme_options": {
                "single_page": True
            },
            # --individualpages option set to True for page call
            "latex_individualpages": True,
        }
    # Build Project
    else:
        build_type = "book"
        PAGE_NAME = None
        BUILD_PATH = Path(BUILD_PATH).joinpath("_build")

        # Table of contents
        toc = PATH_SRC_FOLDER.joinpath("_toc.yml") if toc is None else Path(
            toc)

        if not get_config_only:

            if not toc.exists():
                _error("Couldn't find a Table of Contents file. "
                       "To auto-generate one, run:"
                       f"\n\n\tjupyter-book toc from-project {path_source}")

            # we don't need to read the toc here, but do so to control the error message
            try:
                parse_toc_yaml(toc)
            except MalformedError as exc:
                _error(f"The Table of Contents file is malformed: {exc}\n"
                       "You may need to migrate from the old format, using:"
                       f"\n\n\tjupyter-book toc migrate {toc} -o {toc}")
            # TODO could also check/warn if the format is not set to jb-article/jb-book?

        config_overrides["external_toc_path"] = toc.as_posix()

        # Builder-specific overrides
        if builder == "pdfhtml":
            config_overrides["html_theme_options"] = {"single_page": True}

        # --individualpages option passthrough
        config_overrides["latex_individualpages"] = individualpages

    # Use the specified configuration file, or one found in the root directory
    path_config = config or (found_config[0].joinpath("_config.yml")
                             if found_config[1] else None)
    if path_config and not Path(path_config).exists():
        raise IOError(f"Config file path given, but not found: {path_config}")

    if builder in ["html", "pdfhtml", "linkcheck"]:
        OUTPUT_PATH = BUILD_PATH.joinpath("html")
    elif builder in ["latex", "pdflatex"]:
        OUTPUT_PATH = BUILD_PATH.joinpath("latex")
    elif builder in ["dirhtml"]:
        OUTPUT_PATH = BUILD_PATH.joinpath("dirhtml")
    elif builder in ["singlehtml"]:
        OUTPUT_PATH = BUILD_PATH.joinpath("singlehtml")
    elif builder in ["custom"]:
        OUTPUT_PATH = BUILD_PATH.joinpath(custom_builder)
        BUILDER_OPTS["custom"] = custom_builder

    if nitpick:
        config_overrides["nitpicky"] = True

    # If we only wan config (e.g. for printing/validation), stop here
    if get_config_only:
        return (path_config, PATH_SRC_FOLDER, config_overrides)

    # print information about the build
    click.echo(
        click.style("Source Folder: ", bold=True, fg="blue") +
        click.format_filename(f"{PATH_SRC_FOLDER}"))
    click.echo(
        click.style("Config Path: ", bold=True, fg="blue") +
        click.format_filename(f"{path_config}"))
    click.echo(
        click.style("Output Path: ", bold=True, fg="blue") +
        click.format_filename(f"{OUTPUT_PATH}"))

    # Now call the Sphinx commands to build
    result = build_sphinx(
        PATH_SRC_FOLDER,
        OUTPUT_PATH,
        use_external_toc=use_external_toc,
        noconfig=True,
        path_config=path_config,
        confoverrides=config_overrides,
        builder=BUILDER_OPTS[builder],
        warningiserror=warningiserror,
        keep_going=keep_going,
        freshenv=freshenv,
        verbosity=verbose,
        quiet=quiet > 0,
        really_quiet=quiet > 1,
    )

    builder_specific_actions(result, builder, OUTPUT_PATH, build_type,
                             PAGE_NAME, click.echo)