Moving structure of reader renderer to enable single page or multiple page documents. The toc gets computed on the fly by parsing the html for headers and links are added for document scroll hotlinking.

This commit is contained in:
Robert Tice
2018-02-22 14:47:36 -05:00
parent a14abff8a9
commit fec9ab91fc
19 changed files with 260 additions and 177 deletions

View File

@@ -33,7 +33,7 @@ class Command(BaseCommand):
return
# Look for 'chapters.yaml'
chapters_yaml = os.path.join(d, 'chapters.yaml')
chapters_yaml = os.path.join(d, 'info.yaml')
if not os.path.isfile(chapters_yaml):
print 'There is no "chapters.yaml" in {}'.format(d)
return
@@ -55,5 +55,6 @@ class Command(BaseCommand):
b = Book(description=metadata['description'],
directory=d,
subpath=os.path.basename(os.path.normpath(d)),
title=metadata['title'])
title=metadata['title'],
author=metadata['author'])
b.save()

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2018-02-20 17:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('app', '0003_book'),
]
operations = [
migrations.AddField(
model_name='book',
name='author',
field=models.TextField(default=''),
preserve_default=False,
),
]

View File

@@ -65,4 +65,7 @@ class Book(models.Model):
description = models.TextField()
# the title of the book
title = models.TextField()
title = models.TextField()
# the author of the book
author = models.TextField()

View File

@@ -11,7 +11,7 @@ body {
p {
font-family: 'Poppins', sans-serif;
font-size: 1.1em;
font-size: 0.9em;
font-weight: 300;
line-height: 1.7em;
color: #999;
@@ -56,7 +56,7 @@ a, a:hover, a:focus {
#sidebar {
min-width: 250px;
max-width: 250px;
background: #7386D5;
background: #333333;
color: #fff;
transition: all 0.3s;
}
@@ -67,7 +67,6 @@ a, a:hover, a:focus {
#sidebar .sidebar-header {
padding: 20px;
background: #6d7fcc;
}
#sidebar ul.components {
@@ -82,7 +81,7 @@ a, a:hover, a:focus {
#sidebar ul li a {
padding: 10px;
font-size: 1.1em;
font-size: 0.9em;
display: block;
}
#sidebar ul li a:hover {
@@ -92,7 +91,7 @@ a, a:hover, a:focus {
#sidebar ul li.active > a, a[aria-expanded="true"] {
color: #fff;
background: #6d7fcc;
background: #777777;
}
@@ -112,7 +111,7 @@ a[aria-expanded="true"]::before {
content: '\e260';
}
/*
ul ul a {
font-size: 0.9em !important;
padding-left: 30px !important;
@@ -140,13 +139,14 @@ a.article, a.article:hover {
background: #6d7fcc !important;
color: #fff !important;
}
*/
/* ---------------------------------------------------
CONTENT STYLE
----------------------------------------------------- */
#content {
#reader_content {
margin: 20px;
padding: 20px;
min-height: 100vh;
transition: all 0.3s;

View File

@@ -1 +0,0 @@
@media (min-width:768px){.container{width:503px}}@media (min-width:992px){.container{width:723px}}@media (min-width:1200px){.container{width:923px}}@media (min-width:1432px){.container{width:1170px}}body{padding-top:70px;min-height:200vh}.navbar-fixed-left,.navbar-fixed-right{position:fixed;top:0;width:100%;z-index:1030}@media (min-width:768px) and (min-width:768px){.navbar-fixed-left,.navbar-fixed-right{width:232px;height:100vh;border-radius:0}.navbar-fixed-left .container,.navbar-fixed-right .container{padding-right:0;padding-left:0;width:auto}.navbar-fixed-left .navbar-header,.navbar-fixed-right .navbar-header{padding-left:15px;padding-right:15px}.navbar-fixed-left .navbar-collapse,.navbar-fixed-right .navbar-collapse{padding-right:0;padding-left:0;max-height:none}.navbar-fixed-left .navbar-collapse .navbar-nav,.navbar-fixed-right .navbar-collapse .navbar-nav{float:none!important}.navbar-fixed-left .navbar-collapse .navbar-nav>li,.navbar-fixed-right .navbar-collapse .navbar-nav>li{width:100%}.navbar-fixed-left .navbar-collapse .navbar-nav>li.dropdown .dropdown-menu,.navbar-fixed-right .navbar-collapse .navbar-nav>li.dropdown .dropdown-menu{top:0}.navbar-fixed-left .navbar-collapse .navbar-nav.navbar-right,.navbar-fixed-right .navbar-collapse .navbar-nav.navbar-right{margin-right:0}}@media (min-width:768px){body{padding-top:0;padding-left:232px}.navbar-fixed-left{right:auto!important;left:0!important;border-width:0 1px 0 0!important}.navbar-fixed-left .dropdown .dropdown-menu{left:100%;right:auto;border-radius:0 3px 3px 0}.navbar-fixed-left .dropdown .dropdown-toggle .caret{border-top:4px solid transparent;border-left:4px solid;border-bottom:4px solid transparent;border-right:none}}

View File

@@ -0,0 +1,110 @@
@media (min-width:768px) {
.container {
width: 503px
}
}
@media (min-width:992px) {
.container {
width: 723px
}
}
@media (min-width:1200px) {
.container {
width: 923px
}
}
@media (min-width:1432px) {
.container {
width: 1170px
}
}
body {
padding-top: 70px;
min-height: 200vh
}
.navbar-fixed-left,
.navbar-fixed-right {
position: fixed;
top: 0;
width: 100%;
z-index: 1030;
}
#toc {
height: 100vh;
overflow: hidden;
}
#toc:hover, #toc:active, #toc:focus {
overflow: auto;
}
@media (min-width:768px) and (min-width:768px) {
.navbar-fixed-left,
.navbar-fixed-right {
width: 232px;
height: 100vh;
border-radius: 0
}
.navbar-fixed-left .container,
.navbar-fixed-right .container {
padding-right: 0;
padding-left: 0;
width: auto
}
.navbar-fixed-left .navbar-header,
.navbar-fixed-right .navbar-header {
padding-left: 15px;
padding-right: 15px
}
.navbar-fixed-left .navbar-collapse,
.navbar-fixed-right .navbar-collapse {
padding-right: 0;
padding-left: 0;
max-height: none
}
.navbar-fixed-left .navbar-collapse .navbar-nav,
.navbar-fixed-right .navbar-collapse .navbar-nav {
float: none!important
}
.navbar-fixed-left .navbar-collapse .navbar-nav>li,
.navbar-fixed-right .navbar-collapse .navbar-nav>li {
width: 100%
}
.navbar-fixed-left .navbar-collapse .navbar-nav>li.dropdown .dropdown-menu,
.navbar-fixed-right .navbar-collapse .navbar-nav>li.dropdown .dropdown-menu {
top: 0
}
.navbar-fixed-left .navbar-collapse .navbar-nav.navbar-right,
.navbar-fixed-right .navbar-collapse .navbar-nav.navbar-right {
margin-right: 0
}
}
@media (min-width:768px) {
body {
padding-top: 0;
padding-left: 232px
}
.navbar-fixed-left {
right: auto!important;
left: 0!important;
border-width: 0 1px 0 0!important
}
.navbar-fixed-left .dropdown .dropdown-menu {
left: 100%;
right: auto;
border-radius: 0 3px 3px 0
}
.navbar-fixed-left .dropdown .dropdown-toggle .caret {
border-top: 4px solid transparent;
border-left: 4px solid;
border-bottom: 4px solid transparent;
border-right: none
}
}

View File

@@ -1,40 +1,13 @@
$( document ).ready(function() {
$( '.sidebar-link' ).click(function(event) {
var linkid = event.target.id;
var page_number = linkid.substr(12)
var divsearch = "#page_anchor_" + page_number;
$(document).ready( function() {
if($( divsearch ).length) {
$( 'html, body' ).animate({
scrollTop: $( divsearch ).offset().top
}, 500);
}
else {
infinite_scroll(divsearch);
}
$( ".toc_link" ).click( function() {
var lid = $( this ).attr( 'id' );
var hid = "#header" + lid.substr(5);
$( 'html, body' ).animate( {
scrollTop: $( hid ).offset().top
}, 800);
});
function infinite_scroll(search_elem) {
$('html, body').animate({
scrollTop: $( document ).height()
}, 500, function() {
if(!$( search_elem ).length) {
infinite_scroll(search_elem);
}
});
}
var infinite = new Waypoint.Infinite({
element: $( '.infinite-container' )[0],
onBeforePageLoad: function () {
$( '.loading' ).show();
},
onAfterPageLoad: function ($items) {
$( '.loading' ).hide();
}
});
});
});

View File

@@ -4,15 +4,11 @@
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Ada for the C Programmer</title>
<title>Book Reader</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="{% static "navbar-fixed-left.min.css" %}" />
<link rel="stylesheet" href="{% static "common.css" %}" />
<link rel="stylesheet" href="{% static "book_base.css" %}" />
<script src="{% static "jquery-3.2.1.min.js" %}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/waypoints/4.0.1/jquery.waypoints.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/waypoints/4.0.1/shortcuts/infinite.min.js"></script>
<link rel="stylesheet" href="{% static "sidebar.css" %}" />
</head>
<body>
@@ -23,6 +19,7 @@
</div>
<script src="{% static "jquery-3.2.1.min.js" %}"></script>
<script src="{% static "ace-builds/src/ace.js" %}" type="text/javascript" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>

View File

@@ -22,6 +22,7 @@
<tr class='clickable-row' data-href="{{ HTTP_HOST }}/books/{{ b.subpath }}">
<td>{{ b.title }}</td>
<td>{{ b.description }}</td>
<td>{{ b.author }}</td>
</tr>
{% endfor %}
</tbody>
@@ -33,7 +34,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="{% static "editors.js" %}"></script>
<script src="{% static "sidebar.js" %}"></script>
<script type=""text/javascript">
jQuery(document).ready(function($) {
$(".clickable-row").click(function() {

View File

@@ -1,7 +1,7 @@
{% extends 'book_base.html' %}
{% block sidebar %}
<nav class="navbar navbar-inverse navbar-fixed-left">
<nav id="sidebar" class="navbar navbar-inverse navbar-fixed-left">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
@@ -14,28 +14,7 @@
<a href="{{ HTTP_HOST }}/books/">Back to book list</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
{% for p in parts %}
{% if forloop.counter == sel_part %}
<li class="active">
{% else %}
<li>
{% endif %}
<a href="#partSubmenu{{ forloop.counter }}">{{ p.title }}</a>
<ul id="partSubmenu{{ forloop.counter }}">
{% for c in p.chapters %}
{% if forloop.counter == sel_chapter %}
<li class="active">
{% else %}
<li>
{% endif %}
<a id="sidebar-page{{ c.page }}" class="sidebar-link" href="#">{{ c.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
<div id="toc">{{ sidebar|safe }}</div>
</div>
</div>
</nav>

View File

@@ -1,31 +1,6 @@
{% load markdown_filter %}
{% load rst_filter %}
{% block reader %}
<div id="content">
<div class="infinite-container">
<div class="infinite-item">
<div id="page_anchor_{{ chapter_obj.number }}" class="waypoint_anchor"></div>
{% if mdcontent %}
{{ mdcontent|markdownify|safe }}
{% elif rstcontent %}
{{ rstcontent|rstify|safe }}
{% endif %}
</div>
</div>
<div class="loading" style="display: none;">
Loading...
</div>
{% if chapter_obj.has_next %}
<a class="infinite-more-link" href="{{ HTTP_HOST }}/books/{{ book_info.subpath }}?page={{ chapter_obj.next_page_number }}">Next</a>
{% endif %}
{{ content|safe }}
</div>
{% endblock%}

View File

@@ -5,4 +5,6 @@ register = template.Library()
@register.filter
def rstify(text):
return docutils.core.publish_parts(text, writer_name='html')['html_body']
html = docutils.core.publish_parts(text, writer_name='html')['html_body']
print html
return html

View File

@@ -4,14 +4,15 @@ from __future__ import unicode_literals
from django.shortcuts import render
# Create your views here.
from bs4 import BeautifulSoup
import docutils
import markdown
import os
import yaml
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.views.decorators.clickjacking import xframe_options_exempt
from rest_framework import viewsets, status
@@ -119,6 +120,62 @@ def book_list(request):
return render(request, 'book_list.html', booklist)
def md_filter(text):
return markdown.markdown(text)
def rst_filter(text):
return docutils.core.publish_parts(text, writer_name='html')['html_body']
def toc_filter(htmldata):
def append_li(ul, i, h):
h['id'] = "header" + str(i)
new_link_tag = toc_soup.new_tag('a', href='#')
new_link_tag.string = h.string
new_link_tag['class'] = "toc_link"
new_link_tag['id'] = "hlink" + str(i)
new_li_tag = toc_soup.new_tag('li')
new_li_tag.append(new_link_tag)
ul.append(new_li_tag)
def append_ul(ul):
new_ul = toc_soup.new_tag('ul')
ul.append(new_ul)
return new_ul
prev_level = 1
reader_soup = BeautifulSoup(htmldata['content'], "html.parser")
toc_soup = BeautifulSoup(htmldata['sidebar'], "html.parser")
current_ul = toc_soup.ul
headers = reader_soup.find_all(['h1', 'h2'])
for i, h in enumerate(headers):
cur_level = int(h.name[1:])
if cur_level < prev_level:
outer_ul = current_ul.find_parent('ul')
if outer_ul is not None:
prev_level = cur_level
current_ul = outer_ul
elif cur_level > prev_level:
prev_level = cur_level
current_ul = append_ul(current_ul)
append_li(current_ul, i, h)
htmldata['content'] = str(reader_soup)
htmldata['sidebar'] = str(toc_soup)
return htmldata
def book_router(request, subpath):
resources_base_path = os.path.join(settings.RESOURCES_DIR, "books")
@@ -132,8 +189,8 @@ def book_router(request, subpath):
book = serializer.data
# open chapters list of book
with open(os.path.join(book['directory'], "chapters.yaml"), 'r') as f:
# open book info
with open(os.path.join(book['directory'], "info.yaml"), 'r') as f:
try:
bookdata = yaml.load(f.read())
except:
@@ -142,52 +199,28 @@ def book_router(request, subpath):
return
# store chapters and parts list in htmldata
htmldata = bookdata
htmldata = {}
htmldata['book_info'] = book
# strip chapters out of list into new list for prev, next references
chapter_list = []
for p in bookdata['parts']:
chapter_list.extend(p['chapters'])
htmldata['content'] = ''
paginator = Paginator(chapter_list, 1)
page = request.GET.get('page', 1)
htmldata['sidebar'] = '<ul></ul>'
try:
chapter_obj = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
chapter_obj = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
chapter_obj = paginator.page(paginator.num_pages)
# get list of pages from info.yaml, convert to html, and concat into string
for page in bookdata['pages']:
filepath = os.path.join(book['directory'], page)
filename, file_ext = os.path.splitext(filepath)
if os.path.isfile(filepath):
with open(filepath, 'r') as f:
content = f.read()
if file_ext == '.md':
htmldata['content'] += md_filter(content)
elif file_ext == '.rst':
htmldata['content'] += rst_filter(content)
else:
with open(os.path.join(resources_base_path, "under-construction.md")) as f:
htmldata['content'] += md_filter(f.read())
htmldata['chapter_obj'] = chapter_obj
htmldata = toc_filter(htmldata)
chapter = chapter_obj.object_list[0]
mdcontent_page = os.path.join(book['directory'],
"pages",
"%s.md" % (chapter["url"]))
rstcontent_page = os.path.join(book['directory'],
"pages",
"%s.rst" % (chapter["url"]))
# check for markdown version
if os.path.isfile(mdcontent_page):
with open(mdcontent_page, 'r') as f:
htmldata['mdcontent'] = f.read()
elif os.path.isfile(rstcontent_page):
with open(rstcontent_page, 'r') as f:
htmldata['rstcontent'] = f.read()
else:
with open(os.path.join(resources_base_path,
"under-construction.md")) as f:
htmldata['mdcontent'] = f.read()
if request.is_ajax():
template = 'readerpage.html'
else:
template = 'book_sidebar.html'
return render(request, template, htmldata)
return render(request, 'book_sidebar.html', htmldata)

View File

@@ -1,22 +0,0 @@
---
title: "Example Book"
description: "This is an example book that is a placeholder."
parts:
- title: "Part 1"
chapters:
- title: "Chapter 1"
url: "part1-chapter1"
page: 1
- title: "Chapter 2"
url: "part1-chapter2"
page: 2
- title: "Part 2"
chapters:
- title: "Chapter 1"
url: "part2-chapter1"
page: 3
- title: "Chapter 2"
url: "part2-chapter2"
page: 4
...

View File

@@ -0,0 +1,11 @@
---
title: "Example Book"
description: "This is an example book that is a placeholder."
author: "Joe Schmo"
pages:
- "part1-chapter1.md"
- "part1-chapter2.rst"
- "part2-chapter1.md"
- "part2-chapter2.rst"
...

View File

@@ -1,10 +1,10 @@
Part 1 Chapter 2
=========================
*****************
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id massa felis. Nam eget risus sit amet ante tempor lacinia. Mauris ut nunc sem. Cras mattis, nibh quis fermentum porttitor, arcu tortor porttitor magna, ac adipiscing quam urna at lectus. Ut at dolor in elit tempor ultrices sagittis sed lacus. Nullam a lectus mauris. Pellentesque molestie, leo in auctor semper, magna sem mattis tellus, a consectetur nisl tellus volutpat quam. Etiam ultricies risus sed sapien convallis aliquet. Curabitur vehicula purus vitae justo commodo facilisis. Quisque at porta ipsum. Sed purus leo, mattis sed ultricies ac, ultricies eget lacus. Sed ac nibh est. Suspendisse sed orci nisl. Vestibulum ultrices metus sapien, sed interdum nunc. In arcu neque, sollicitudin ut porta eu, viverra at elit. Nam accumsan condimentum metus nec accumsan. Nunc porta consectetur nisi in ornare. Vestibulum tempor mollis dui quis luctus. Aliquam dolor enim, tristique a blandit eu, auctor ut odio.
Test
~~~~
======
This is a test of inserting code blocks in the RST