summaryrefslogtreecommitdiffstats
path: root/lib/parse/lex.js
blob: 852d4e00b2b667137355bed44715b784837ed4f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
var _ = require('lodash');
var marked = require('marked');

// Split a page up into sections (lesson, exercises, ...)
function splitSections(nodes) {
    var section = [];

    return _.reduce(nodes, function(sections, el) {
        if(el.type === 'hr') {
            sections.push(section);
            section = [];
        } else {
            section.push(el);
        }

        return sections;
    }, []).concat([section]); // Add remaining nodes
}

function isQuizNode(node) {
    return (/^[(\[][ x][)\]]/).test(node.text || node);
}

function isExercise(nodes) {
    var codeType = { type: 'code' };

    // Number of code nodes in section
    var len = _.filter(nodes, codeType).length;

    return (
        // Got 3 or 4 code blocks
        (len === 3 || len === 4) &&
        // Ensure all nodes are at the end
        _.all(_.last(nodes, len), codeType)
    );
}

function isQuiz(nodes) {
    if (nodes.length < 3) {
        return false;
    }

    // Support having a first paragraph block
    // before our series of questions
    var quizNodes = nodes.slice(nodes[0].type === 'paragraph' ? 1 : 0);

    // No questions
    if (!_.some(quizNodes, { type: 'blockquote_start' })) {
        return false;
    }

    // Check if section has list of questions
    // or table of questions
    var listIdx = _.findIndex(quizNodes, { type: 'list_item_start' });
    var tableIdx = _.findIndex(quizNodes, { type: 'table' });

    if(
        // List of questions
        listIdx !== -1 && isQuizNode(quizNodes[listIdx + 1]) ||

        // Table of questions
        (
            tableIdx !== -1 &&
            _.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode)
        )
    ) {
        return true;
    }

    return false;
}

// What is the type of this section
function sectionType(nodes, idx) {
    if(isExercise(nodes)) {
        return 'exercise';
    } else if(isQuiz(nodes)) {
        return 'quiz';
    }

    return 'normal';
}

// Generate a uniqueId to identify this section in our code
function sectionId(section, idx) {
    return _.uniqueId('gitbook_');
}

function lexPage(src) {
    // Lex file
    var nodes = marked.lexer(src);

    return _.chain(splitSections(nodes))
    .map(function(section, idx) {
        // Detect section type
        section.type = sectionType(section, idx);
        return section;
    })
    .map(function(section, idx) {
        // Give each section an ID
        section.id = sectionId(section, idx);
        return section;

    })
    .filter(function(section) {
        return !_.isEmpty(section);
    })
    .reduce(function(sections, section) {
        var last = _.last(sections);

        // Merge normal sections together
        if(last && last.type === section.type && last.type === 'normal') {
            last.push.apply(last, [{'type': 'hr'}].concat(section));
        } else {
            // Add to list of sections
            sections.push(section);
        }

        return sections;
    }, [])
    .map(function(section) {
        section.links = nodes.links;
        return section;
    })
    .value();
}

// Exports
module.exports = lexPage;