diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a882442 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/src/main/java/com/neo4j/sandbox/updater/BatchUpdater.java b/src/main/java/com/neo4j/sandbox/updater/BatchUpdater.java index a8306b4..16e0e6a 100644 --- a/src/main/java/com/neo4j/sandbox/updater/BatchUpdater.java +++ b/src/main/java/com/neo4j/sandbox/updater/BatchUpdater.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -75,7 +76,9 @@ public void updateBatch() throws IOException { Path cloneLocation = cloneLocationsBaseDir.resolve(repositoryName(repositoryUri)); String branch = null; try { - List updatedFiles = updater.updateCodeExamples(settings.getCodeSamplesPath(), cloneLocation, repositoryUri); + List updatedFiles = new ArrayList<>(); + updatedFiles.addAll(updater.updateCodeExamples(settings.getCodeSamplesPath(), cloneLocation, repositoryUri)); + updatedFiles.addAll(updater.generateBrowserGuides(cloneLocation, repositoryUri)); branch = generateConsistentBranchName(repositoryName(repositoryUri), updatedFiles); createPullRequest(repositoryUri, cloneLocation, branch); } catch (BlankCommitException exception) { diff --git a/src/main/java/com/neo4j/sandbox/updater/BrowserGuideConverter.java b/src/main/java/com/neo4j/sandbox/updater/BrowserGuideConverter.java new file mode 100644 index 0000000..7f09ccc --- /dev/null +++ b/src/main/java/com/neo4j/sandbox/updater/BrowserGuideConverter.java @@ -0,0 +1,31 @@ +package com.neo4j.sandbox.updater; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Options; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.net.URISyntaxException; +import java.util.Objects; + +@Service +public class BrowserGuideConverter { + + private final File browserGuideTemplatesDir; + + private final Asciidoctor parser; + + public BrowserGuideConverter(Asciidoctor parser) throws URISyntaxException { + this.parser = parser; + this.browserGuideTemplatesDir = new File(Objects.requireNonNull(BrowserGuideConverter.class.getResource("/templates/browser-guide")).toURI()); + } + + public String convert(File asciiDocFile) { + Options asciidoctorOptions = Options.builder() + .templateDirs(this.browserGuideTemplatesDir) + .headerFooter(true) + .toFile(false) + .build(); + return parser.convertFile(asciiDocFile, asciidoctorOptions); + } +} diff --git a/src/main/java/com/neo4j/sandbox/updater/Updater.java b/src/main/java/com/neo4j/sandbox/updater/Updater.java index a8d8446..0d6aa6a 100644 --- a/src/main/java/com/neo4j/sandbox/updater/Updater.java +++ b/src/main/java/com/neo4j/sandbox/updater/Updater.java @@ -2,20 +2,18 @@ import com.neo4j.sandbox.git.GitOperations; import com.neo4j.sandbox.github.GithubSettings; -import com.neo4j.sandbox.updater.formatting.DefaultQueryIndenter; -import com.neo4j.sandbox.updater.formatting.IndentDetector; -import com.neo4j.sandbox.updater.formatting.JavaQueryIndenter; -import com.neo4j.sandbox.updater.formatting.QueryIndenter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @Component public class Updater { @@ -26,18 +24,64 @@ public class Updater { private final TemplateEngine templateEngine; + private final BrowserGuideConverter browserGuideConverter; + private final GithubSettings githubSettings; public Updater(GitOperations cloner, MetadataReader metadataReader, + BrowserGuideConverter browserGuideConverter, GithubSettings githubSettings) { this.cloner = cloner; this.templateEngine = new TemplateEngine(metadataReader); + this.browserGuideConverter = browserGuideConverter; this.githubSettings = githubSettings; } + /** + * Generates browser guides. + *

+ * The updater will clone the repository if {@code cloneLocation} does not denote an existing path. + *

+ * + * @param cloneLocation path to the local clone of the sandbox repository (will be created by `git clone` if it + * does not exist) + * @param repositoryUri URI of the sandbox repository + * @return the list of generated browser guides + * @throws IOException if any of the underlying file operations fail + */ + public List generateBrowserGuides(Path cloneLocation, String repositoryUri) throws IOException { + if (cloneLocation.toFile().exists()) { + LOGGER.debug("Clone of {} already exists at location {}. Skipping git clone operation.", repositoryUri, cloneLocation); + } else { + LOGGER.trace("About to clone {} at {}.", repositoryUri, cloneLocation); + this.cloner.clone(cloneLocation, repositoryUri, githubSettings.getToken()); + } + Path documentationFolder = cloneLocation.resolve("documentation"); + if (!documentationFolder.toFile().exists()) { + LOGGER.debug("Folder documentation does not exist in repository {}. Skipping.", repositoryUri); + return new ArrayList<>(); + } + LOGGER.trace("About to generate browser guide for {}.", repositoryUri); + List asciiDocPaths = Files.list(documentationFolder).filter(path -> { + File file = path.toFile(); + return file.isFile() && file.getName().endsWith(".adoc"); + }).collect(Collectors.toList()); + List generatedFiles = new ArrayList<>(asciiDocPaths.size()); + for (Path asciiDocPath : asciiDocPaths) { + File asciiDocFile = asciiDocPath.toFile(); + LOGGER.trace("About to convert {} of {} to browser guide.", asciiDocFile, repositoryUri); + String content = browserGuideConverter.convert(asciiDocFile); + String outputFileName = asciiDocFile.getName().replaceFirst("\\.adoc$", ".neo4j-browser-guide"); + Path browserGuidePath = documentationFolder.resolve(outputFileName); + Files.write(browserGuidePath, content.getBytes(StandardCharsets.UTF_8)); + generatedFiles.add(browserGuidePath); + } + return generatedFiles; + } + /** * Updates every code sample of the provided sandbox repository. *

diff --git a/src/main/resources/templates/browser-guide/block_admonition.html.erb b/src/main/resources/templates/browser-guide/block_admonition.html.erb new file mode 100644 index 0000000..215b430 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_admonition.html.erb @@ -0,0 +1,21 @@ +<%#encoding:UTF-8%> class="<%= ['admonitionblock',(attr :name),role].compact * ' ' %>"> + + + + + +
<% +if @document.attr? :icons, 'font' %> +<% +elsif @document.attr? :icons %> +<%= attr 'textlabel' %><% +else %> +
<%= attr 'textlabel' %>
<% +end %> +
<% +if title? %> +
<%= title %>
<% +end %> +<%= content %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_audio.html.erb b/src/main/resources/templates/browser-guide/block_audio.html.erb new file mode 100644 index 0000000..aad0049 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_audio.html.erb @@ -0,0 +1,13 @@ +<%#encoding:UTF-8%> class="<%= ['audioblock',@style,role].compact * ' ' %>"><% +if title? %> +

<%= captioned_title %>
<% +end %> +
+ +
+ diff --git a/src/main/resources/templates/browser-guide/block_colist.html.erb b/src/main/resources/templates/browser-guide/block_colist.html.erb new file mode 100644 index 0000000..9b5aed3 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_colist.html.erb @@ -0,0 +1,28 @@ +<%#encoding:UTF-8%> class="<%= ['colist',@style,role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end +if @document.attr? :icons + font_icons = @document.attr? :icons, 'font' %> +<% + items.each_with_index do |item, i| + num = i + 1 %> + + + +<% + end %> +
<% + if font_icons %><%= num %><% + else %><%= num %><% + end %><%= item.text %>
<% +else %> +
    <% + items.each do |item| %> +
  1. +

    <%= item.text %>

    +
  2. <% + end %> +
<% +end %> + diff --git a/src/main/resources/templates/browser-guide/block_dlist.html.erb b/src/main/resources/templates/browser-guide/block_dlist.html.erb new file mode 100644 index 0000000..f302362 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_dlist.html.erb @@ -0,0 +1,87 @@ +<%#encoding:UTF-8%><% +case @style +when 'qanda' +%> class="<%= ['qlist','qanda',role].compact * ' ' %>"><% + if title? %> +
<%= title %>
<% + end %> +
    <% + items.each do |questions, answer| %> +
  1. <% + [*questions].each do |question| %> +

    <%= question.text %>

    <% + end + unless answer.nil? + if answer.text? %> +

    <%= answer.text %>

    <% + end + if answer.blocks? %> +<%= answer.content %><% + end + end %> +
  2. <% + end %> +
+<% +when 'horizontal' +%> class="<%= ['hdlist',role].compact * ' ' %>"><% + if title? %> +
<%= title %>
<% + end %> +<% + if (attr? :labelwidth) || (attr? :itemwidth) %> ++> +> +<% + end + items.each do |terms, dd| %> + + + +<% + end %> +
<% + terms = [*terms] + last_term = terms.last + terms.each do |dt| %> +<%= dt.text %><% + if dt != last_term %> +
<% + end + end %> +
<% + unless dd.nil? + if dd.text? %> +

<%= dd.text %>

<% + end + if dd.blocks? %> +<%= dd.content %><% + end + end %> +
+<% +else +%> class="<%= ['dlist',@style,role].compact * ' ' %>"><% + if title? %> +
<%= title %>
<% + end %> +
<% + items.each do |terms, dd| + [*terms].each do |dt| %> +><%= dt.text %><% + end + unless dd.nil? %> +
<% + if dd.text? %> +

<%= dd.text %>

<% + end + if dd.blocks? %> +<%= dd.content %><% + end %> +
<% + end + end %> +
+<% +end %> diff --git a/src/main/resources/templates/browser-guide/block_example.html.erb b/src/main/resources/templates/browser-guide/block_example.html.erb new file mode 100644 index 0000000..ea986eb --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_example.html.erb @@ -0,0 +1,8 @@ +<%#encoding:UTF-8%> class="<%= ['exampleblock',role].compact * ' ' %>"><% +if title? %> +
<%= captioned_title %>
<% +end %> +
+<%= content %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_floating_title.html.erb b/src/main/resources/templates/browser-guide/block_floating_title.html.erb new file mode 100644 index 0000000..a98d704 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_floating_title.html.erb @@ -0,0 +1 @@ +<%#encoding:UTF-8%><%= %(#{title}) %> diff --git a/src/main/resources/templates/browser-guide/block_image.html.erb b/src/main/resources/templates/browser-guide/block_image.html.erb new file mode 100644 index 0000000..258d2e3 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_image.html.erb @@ -0,0 +1,15 @@ +<%#encoding:UTF-8%> class="<%= ['imageblock',@style,role].compact * ' ' %>"<% +if (attr? :align) || (attr? :float) +%> style="<%= [("text-align: #{attr :align};" if attr? :align),("float: #{attr :float};" if attr? :float)].compact * ' ' %>"<% +end %>> +
<% +if attr? :link %> +<%= attr :alt %><%= (attr? :height) ? %( height="#{attr :height}") : nil %>><% +else %> +<%= attr :alt %><%= (attr? :height) ? %( height="#{attr :height}") : nil %>><% +end %> +
<% +if title? %> +
<%= captioned_title %>
<% +end %> + diff --git a/src/main/resources/templates/browser-guide/block_listing.html.erb b/src/main/resources/templates/browser-guide/block_listing.html.erb new file mode 100644 index 0000000..7a23b13 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_listing.html.erb @@ -0,0 +1,41 @@ +<%#encoding:UTF-8%> class="<%= ['listingblock',role].compact * ' ' %>"><% +if title? %> +
<%= captioned_title %>
<% +end %> +
<% +nowrap = !(@document.attr? :prewrap) || (option? :nowrap) +if @style == 'source' + language = attr :language + code_class = language ? [language, %(language-#{language})] : [] + pre_class = ['highlight'] + pre_lang = language + case attr 'source-highlighter' + when 'coderay' + pre_class = ['CodeRay'] + when 'pygments' + pre_class = ['pygments','highlight'] + when 'prettify' + pre_class = ['prettyprint'] + pre_class << 'linenums' if attr? :linenums + pre_class << language if language + pre_class << %(language-#{language}) if language + code_class = [] + when 'html-pipeline' + pre_lang = language + pre_class = code_class = [] + nowrap = false + end + pre_class << "pre-scrollable" + pre_class << "programlisting" + pre_class << "cm-s-neo" + pre_class << "code" + pre_class << "runnable" + pre_class << "standalone-example" + pre_class << "ng-binding" + pre_class << 'nowrap' if nowrap %> +
<%= pre_lang && %( data-lang="#{pre_lang}") %><%= pre_lang && %( lang="#{pre_lang}") %>><%= content %>
<% +else %> +><%= content %><% +end %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_literal.html.erb b/src/main/resources/templates/browser-guide/block_literal.html.erb new file mode 100644 index 0000000..17a4383 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_literal.html.erb @@ -0,0 +1,8 @@ +<%#encoding:UTF-8%> class="<%= ['literalblock',role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end %> +
+><%= content %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_math.html.erb b/src/main/resources/templates/browser-guide/block_math.html.erb new file mode 100644 index 0000000..b1f79ce --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_math.html.erb @@ -0,0 +1,17 @@ +<%#encoding:UTF-8%> class="<%= ['mathblock',role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end %> +
<% +open, close = Asciidoctor::BLOCK_MATH_DELIMITERS[@style.to_sym] +equation = content.strip +if (@subs.nil? || @subs.empty?) && !(attr? 'subs') + equation = sub_specialcharacters equation +end +unless (equation.start_with? open) && (equation.end_with? close) + equation = %(#{open}#{equation}#{close}) +end +%> +<%= %(#{equation}\n) %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_olist.html.erb b/src/main/resources/templates/browser-guide/block_olist.html.erb new file mode 100644 index 0000000..fc50f2a --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_olist.html.erb @@ -0,0 +1,15 @@ +<%#encoding:UTF-8%> class="<%= ['olist',@style,role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end %> +
    <%= (keyword = list_marker_keyword) && %( type="#{keyword}") %>><% +items.each do |item| %> +
  1. +

    <%= item.text %>

    <% + if item.blocks? %> +<%= item.content %><% + end %> +
  2. <% +end %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_open.html.erb b/src/main/resources/templates/browser-guide/block_open.html.erb new file mode 100644 index 0000000..4163c91 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_open.html.erb @@ -0,0 +1,26 @@ +<%#encoding:UTF-8%><% +if @style == 'abstract' + if @parent == @document && @document.doctype == 'book' + puts 'asciidoctor: WARNING: abstract block cannot be used in a document without a title when doctype is book. Excluding block content.' + else +%> class="<%= ['quoteblock','abstract',role].compact * ' ' %>"><% + if title? %> +
<%= title %>
<% + end %> +
+<%= content %> +
+<% + end +elsif @style == 'partintro' && (@level != 0 || @parent.context != :section || @document.doctype != 'book') + puts 'asciidoctor: ERROR: partintro block can only be used when doctype is book and it\'s a child of a book part. Excluding block content.' +else +%> class="<%= ['openblock',(@style == 'open' ? nil : @style),role].compact * ' ' %>"><% + if title? %> +
<%= title %>
<% + end %> +
+<%= content %> +
+<% +end %> diff --git a/src/main/resources/templates/browser-guide/block_page_break.html.erb b/src/main/resources/templates/browser-guide/block_page_break.html.erb new file mode 100644 index 0000000..d7a5cc7 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_page_break.html.erb @@ -0,0 +1 @@ +
diff --git a/src/main/resources/templates/browser-guide/block_paragraph.html.erb b/src/main/resources/templates/browser-guide/block_paragraph.html.erb new file mode 100644 index 0000000..7eb8095 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_paragraph.html.erb @@ -0,0 +1,6 @@ +<%#encoding:UTF-8%> class="<%= ['paragraph',role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end %> +

<%= content %>

+ diff --git a/src/main/resources/templates/browser-guide/block_pass.html.erb b/src/main/resources/templates/browser-guide/block_pass.html.erb new file mode 100644 index 0000000..e5f6dda --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_pass.html.erb @@ -0,0 +1 @@ +<%#encoding:UTF-8%><%= content %> diff --git a/src/main/resources/templates/browser-guide/block_preamble.html.erb b/src/main/resources/templates/browser-guide/block_preamble.html.erb new file mode 100644 index 0000000..9cb6e31 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_preamble.html.erb @@ -0,0 +1,11 @@ +<%#encoding:UTF-8%>
+
+<%= content %> +
<% +if (attr? :toc) && (attr? 'toc-placement', 'preamble') %> +
+
<%= attr 'toc-title' %>
+<%= converter.convert @document, 'outline' %> +
<% +end %> +
diff --git a/src/main/resources/templates/browser-guide/block_quote.html.erb b/src/main/resources/templates/browser-guide/block_quote.html.erb new file mode 100644 index 0000000..aec1cc3 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_quote.html.erb @@ -0,0 +1,20 @@ +<%#encoding:UTF-8%> class="<%= ['quoteblock',role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end %> +
+<%= content %> +
<% +if (attr? :attribution) or (attr? :citetitle) %> +
<% + if attr? :citetitle %> +<%= attr :citetitle %><% + end + if attr? :attribution + if attr? :citetitle %>
<% + end %> +<%= "— #{attr :attribution}" %><% + end %> +
<% +end %> + diff --git a/src/main/resources/templates/browser-guide/block_ruler.html.erb b/src/main/resources/templates/browser-guide/block_ruler.html.erb new file mode 100644 index 0000000..e123ba7 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_ruler.html.erb @@ -0,0 +1 @@ +
diff --git a/src/main/resources/templates/browser-guide/block_sidebar.html.erb b/src/main/resources/templates/browser-guide/block_sidebar.html.erb new file mode 100644 index 0000000..37f66fd --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_sidebar.html.erb @@ -0,0 +1,8 @@ +<%#encoding:UTF-8%> class="<%= ['sidebarblock',role].compact * ' ' %>"> +
<% +if title? %> +
<%= title %>
<% +end %> +<%= content %> +
+ diff --git a/src/main/resources/templates/browser-guide/block_table.html.erb b/src/main/resources/templates/browser-guide/block_table.html.erb new file mode 100644 index 0000000..819c827 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_table.html.erb @@ -0,0 +1,55 @@ +<%#encoding:UTF-8%> class="table <%= +['tableblock',"frame-#{attr :frame, 'all'}","grid-#{attr :grid, 'all'}",role].compact * ' ' %>"<% +if (attr? :float) || !(option? :autowidth) %> style="<%= +[("width: #{attr :tablepcwidth}%;" unless option? :autowidth),("float: #{attr :float};" if attr? :float)].compact * ' ' %>"<% +end %>><% +if title? %> +<%= captioned_title %><% +end +unless (attr :rowcount).zero? %> +<% + if option? :autowidth + @columns.size.times do %> +<% + end + else + @columns.each do |col| %> +<% + end + end %> +<% + [:head, :foot, :body].select {|tsec| !@rows[tsec].empty? }.each do |tsec| %> +><% + @rows[tsec].each do |row| %> +<% + row.each do |cell| + # store reference of content in advance to resolve attribute assignments in cells + if tsec == :head + cell_content = cell.text + else + case cell.style + when :verse, :literal + cell_content = cell.text + else + cell_content = cell.content + end + end + cell_css_style = (@document.attr? :cellbgcolor) ? %(background-color: #{@document.attr :cellbgcolor};) : nil %> +<<%= (cell_tag_name = (tsec == :head || cell.style == :header ? 'th' : 'td')) %> class="<%= ['tableblock',"halign-#{cell.attr :halign}","valign-#{cell.attr :valign}"] * ' ' %>"<%= cell.colspan ? %( colspan="#{cell.colspan}") : nil %><%= cell.rowspan ? %( rowspan="#{cell.rowspan}") : nil %><%= cell_css_style ? %( style="#{cell_css_style}") : nil %>><% + if tsec == :head %><%= cell_content %><% + else + case cell.style + when :asciidoc %>
<%= cell_content %>
<% + when :verse %>
<%= cell_content %>
<% + when :literal %>
<%= cell_content %>
<% + else + cell_content.each do |text| %>

<%= text %>

<% end + end + end %>><% + end %> +<% + end %> +
><% + end +end %> + diff --git a/src/main/resources/templates/browser-guide/block_toc.html.erb b/src/main/resources/templates/browser-guide/block_toc.html.erb new file mode 100644 index 0000000..a7a2524 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_toc.html.erb @@ -0,0 +1,17 @@ +<%#encoding:UTF-8%><% +if @document.attr? :toc + toc_id = @id + toc_role = (attr :role, (@document.attr 'toc-class', 'toc')) + toc_title_id = nil + toc_title = title? ? title : (@document.attr 'toc-title') + toc_levels = (attr? :levels) ? (attr :levels).to_i : (@document.attr :toclevels, 2).to_i + if !toc_id && (@document.embedded? || !(@document.attr? 'toc-placement')) + toc_id = 'toc' + toc_title_id = 'toctitle' + end +%> class="<%= toc_role %>"><% + if toc_title %> +
><%= toc_title %>
<% + end %> +<%= converter.convert_with_options @document, 'outline', :toclevels => toc_levels %><% +end %> diff --git a/src/main/resources/templates/browser-guide/block_ulist.html.erb b/src/main/resources/templates/browser-guide/block_ulist.html.erb new file mode 100644 index 0000000..c82cd07 --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_ulist.html.erb @@ -0,0 +1,32 @@ +<%#encoding:UTF-8%><% +if (checklist = (option? :checklist) ? 'checklist' : nil) + if option? :interactive + marker_checked = '' + marker_unchecked = '' + else + if @document.attr? :icons, 'font' + marker_checked = '' + marker_unchecked = '' + else + marker_checked = '✓' + marker_unchecked = '❏' + end + end +end %> class="<%= ['ulist',checklist,@style,role].compact * ' ' %>"><% +if title? %> +
<%= title %>
<% +end %> +><% +items.each do |item| %> +
  • +

    <% + if checklist && (item.attr? :checkbox) %><%= %(#{(item.attr? :checked) ? marker_checked : marker_unchecked} #{item.text}) %><% + else %><%= item.text %><% + end %>

    <% + if item.blocks? %> +<%= item.content %><% + end %> +
  • <% +end %> + + diff --git a/src/main/resources/templates/browser-guide/block_verse.html.erb b/src/main/resources/templates/browser-guide/block_verse.html.erb new file mode 100644 index 0000000..338baac --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_verse.html.erb @@ -0,0 +1,18 @@ +<%#encoding:UTF-8%> class="<%= ['verseblock',role].compact * ' ' %>"><% +if title? %> +
    <%= title %>
    <% +end %> +
    <%= content %>
    <% +if (attr? :attribution) or (attr? :citetitle) %> +
    <% + if attr? :citetitle %> +<%= attr :citetitle %><% + end + if attr? :attribution + if attr? :citetitle %>
    <% + end %> +<%= "— #{attr :attribution}" %><% + end %> +
    <% +end %> + diff --git a/src/main/resources/templates/browser-guide/block_video.html.erb b/src/main/resources/templates/browser-guide/block_video.html.erb new file mode 100644 index 0000000..f654dea --- /dev/null +++ b/src/main/resources/templates/browser-guide/block_video.html.erb @@ -0,0 +1,34 @@ +<%#encoding:UTF-8%> class="<%= ['videoblock',@style,role].compact * ' ' %>"><% +if title? %> +
    <%= captioned_title %>
    <% +end %> +
    <% +case attr :poster +when 'vimeo' + start_anchor = (attr? :start) ? %(#at=#{attr :start}) : nil + delimiter = '?' + autoplay_param = (option? :autoplay) ? %(#{delimiter}autoplay=1) : nil + delimiter = '&' if autoplay_param + loop_param = (option? :loop) ? %(#{delimiter}loop=1) : nil + src = %(//player.vimeo.com/video/#{attr :target}#{start_anchor}#{autoplay_param}#{loop_param}) %> +<%= (attr? :height) ? %( height="#{attr :height}") : nil %> src="<%= src %>" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen><% +when 'youtube' + params = ['rel=0'] + params << %(start=#{attr :start}) if attr? :start + params << %(end=#{attr :end}) if attr? :end + params << 'autoplay=1' if option? :autoplay + params << 'loop=1' if option? :loop + params << 'controls=0' if option? :nocontrols + src = %(//www.youtube.com/embed/#{attr :target}?#{params * '&'}) %> +<%= (attr? :height) ? %( height="#{attr :height}") : nil %> src="<%= src %>" frameborder="0"<%= (option? :nofullscreen) ? nil : ' allowfullscreen' %>><% +else %> +<% +end %> +
    + diff --git a/src/main/resources/templates/browser-guide/document.html.erb b/src/main/resources/templates/browser-guide/document.html.erb new file mode 100644 index 0000000..d03ce3e --- /dev/null +++ b/src/main/resources/templates/browser-guide/document.html.erb @@ -0,0 +1,45 @@ + + +
    + + + <%= content %> + +
    diff --git a/src/main/resources/templates/browser-guide/embedded.html.erb b/src/main/resources/templates/browser-guide/embedded.html.erb new file mode 100644 index 0000000..dc1553f --- /dev/null +++ b/src/main/resources/templates/browser-guide/embedded.html.erb @@ -0,0 +1,14 @@ +<%#encoding:UTF-8%><% +if !notitle && has_header? + %>><%= @header.title %><% +end %><%= content %><% +if footnotes? && !(attr? :nofootnotes) %> +
    +
    <% + footnotes.each do |fn| %><%= %( +
    +#{fn.index}. #{fn.text} +
    ) %><% + end %> +
    <% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_anchor.html.erb b/src/main/resources/templates/browser-guide/inline_anchor.html.erb new file mode 100644 index 0000000..1ba13c0 --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_anchor.html.erb @@ -0,0 +1,9 @@ +<%#encoding:UTF-8%><% +case @type +when :xref + refid = (attr :refid) || @target + %><%= %(#{@text || @document.references[:ids].fetch(refid, %[[#{refid}]]).tr_s("\n", ' ')}) %><% +when :ref %><%= %() %><% +when :bibref %><%= %([#{@target}]) %><% +else %><%= %(#{@text}) %><% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_break.html.erb b/src/main/resources/templates/browser-guide/inline_break.html.erb new file mode 100644 index 0000000..0119fe6 --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_break.html.erb @@ -0,0 +1 @@ +<%= @text %>
    diff --git a/src/main/resources/templates/browser-guide/inline_button.html.erb b/src/main/resources/templates/browser-guide/inline_button.html.erb new file mode 100644 index 0000000..121198a --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_button.html.erb @@ -0,0 +1 @@ +<%#encoding:UTF-8%><%= @text %> diff --git a/src/main/resources/templates/browser-guide/inline_callout.html.erb b/src/main/resources/templates/browser-guide/inline_callout.html.erb new file mode 100644 index 0000000..75bcf75 --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_callout.html.erb @@ -0,0 +1,5 @@ +<%#encoding:UTF-8%><% +if @document.attr? :icons, 'font' %>(<%= @text %>)<% +elsif @document.attr? :icons %>" alt="<%= @text %>"><% +else %>(<%= @text %>)<% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_footnote.html.erb b/src/main/resources/templates/browser-guide/inline_footnote.html.erb new file mode 100644 index 0000000..62ea98a --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_footnote.html.erb @@ -0,0 +1,7 @@ +<%#encoding:UTF-8%><% +idx = attr :index +if @type == :xref +%><%= %([#{idx}]) %><% +else +%><%= %([#{idx}]) %><% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_image.html.erb b/src/main/resources/templates/browser-guide/inline_image.html.erb new file mode 100644 index 0000000..b4ac48d --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_image.html.erb @@ -0,0 +1,18 @@ +<%#encoding:UTF-8%>><% +if @type == 'icon' && (@document.attr? :icons, 'font') + style_class = [%(icon-#{@target})] + style_class << %(icon-#{attr :size}) if attr? :size + style_class << %(icon-rotate-#{attr :rotate}) if attr? :rotate + style_class << %(icon-flip-#{attr :flip}) if attr? :flip + title_attr = (attr? :title) ? %( title="#{attr :title}") : nil + img = %() +elsif @type == 'icon' && !(@document.attr? :icons) + img = %([#{attr :alt}]) +else + img_src = (@type == 'icon' ? (icon_uri @target) : (image_uri @target)) + img_attrs = [:alt, :width, :height, :title].map {|name| (attr? name) ? %( #{name}="#{attr name}") : nil }.join + img = %() +end +if attr? :link %>><%= img %><% +else %><%= img %><% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_indexterm.html.erb b/src/main/resources/templates/browser-guide/inline_indexterm.html.erb new file mode 100644 index 0000000..a215e56 --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_indexterm.html.erb @@ -0,0 +1 @@ +<%#encoding:UTF-8%><%= @type == :visible ? @text : nil %> diff --git a/src/main/resources/templates/browser-guide/inline_kbd.html.erb b/src/main/resources/templates/browser-guide/inline_kbd.html.erb new file mode 100644 index 0000000..907c20f --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_kbd.html.erb @@ -0,0 +1,11 @@ +<%#encoding:UTF-8%><% +keys = attr 'keys' +if keys.size == 1 + %><%= keys.first %><% +else + %><% + idx = 0 + keys.map do |key| + %><%= (idx += 1) == 1 ? nil : '+' %><%= key %><% + end %><% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_menu.html.erb b/src/main/resources/templates/browser-guide/inline_menu.html.erb new file mode 100644 index 0000000..a95b62d --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_menu.html.erb @@ -0,0 +1,12 @@ +<%#encoding:UTF-8%><% +menu = attr 'menu' +submenus = attr 'submenus' +menuitem = attr 'menuitem' +if !submenus.empty? + submenu_path = submenus.map {|submenu| %(#{submenu} ▸ ) }.join.chop + %><%= menu %> ▸ <%= submenu_path %> <%= menuitem %><% +elsif !menuitem.nil? + %><%= menu %> ▸ <%= menuitem %><% +else + %><%= menu %><% +end %> diff --git a/src/main/resources/templates/browser-guide/inline_quoted.html.erb b/src/main/resources/templates/browser-guide/inline_quoted.html.erb new file mode 100644 index 0000000..1193ab5 --- /dev/null +++ b/src/main/resources/templates/browser-guide/inline_quoted.html.erb @@ -0,0 +1,14 @@ +<%#encoding:UTF-8%><%= @id && %() %><% +class_attr = (style_class = role) ? %( class="#{style_class}") : nil +case @type +when :emphasis %><%= %(#{@text}) %><% +when :strong %><%= %(#{@text}) %><% +when :monospaced %><%= %(#{@text}) %><% +when :superscript %><%= %(#{@text}) %><% +when :subscript %><%= %(#{@text}) %><% +when :double %><%= class_attr ? %(“#{@text}”) : %(“#{@text}”) %><% +when :single %><%= class_attr ? %(‘#{@text}’) : %(‘#{@text}’) %><% +when :asciimath, :latexmath + open, close = Asciidoctor::INLINE_MATH_DELIMITERS[@type] %><%= %(#{open}#{@text}#{close}) %><% +else %><%= class_attr ? %(#{@text}) : @text %><% +end %> diff --git a/src/main/resources/templates/browser-guide/section.html.erb b/src/main/resources/templates/browser-guide/section.html.erb new file mode 100644 index 0000000..224ddab --- /dev/null +++ b/src/main/resources/templates/browser-guide/section.html.erb @@ -0,0 +1,15 @@ +<% slevel = @level.zero? && @special ? 1 : @level %> +<% if slevel == 2 %> + +
    +

    <%=title %>

    +
    +
    + <%= content %> +
    +
    +
    +<% else %> +

    <%=title %>

    + <%= content %> +<% end %> diff --git a/src/test/java/com/neo4j/sandbox/GenerateBrowserGuideTest.java b/src/test/java/com/neo4j/sandbox/GenerateBrowserGuideTest.java new file mode 100644 index 0000000..d8f2391 --- /dev/null +++ b/src/test/java/com/neo4j/sandbox/GenerateBrowserGuideTest.java @@ -0,0 +1,23 @@ +package com.neo4j.sandbox; + +import com.neo4j.sandbox.updater.BrowserGuideConverter; +import org.asciidoctor.Asciidoctor; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.net.URISyntaxException; + +import static com.neo4j.sandbox.updater.TestPaths.classpathFile; +import static org.assertj.core.api.Assertions.assertThat; + +public class GenerateBrowserGuideTest { + + @Test + public void converts_browser_guide() throws URISyntaxException { + BrowserGuideConverter browserGuideConverter = new BrowserGuideConverter(Asciidoctor.Factory.create()); + File asciiDocFile = classpathFile("/fake-twitter-repo/documentation/twitter.adoc").toFile(); + String content = browserGuideConverter.convert(asciiDocFile); + assertThat(content).contains("
    "); + assertThat(content).contains("(u:Me:User)-[p:POSTS]->(t:Tweet)-[:MENTIONS]->(m:User)"); + } +} diff --git a/src/test/java/com/neo4j/sandbox/updater/TestPaths.java b/src/test/java/com/neo4j/sandbox/updater/TestPaths.java index 00d81ef..25d4474 100644 --- a/src/test/java/com/neo4j/sandbox/updater/TestPaths.java +++ b/src/test/java/com/neo4j/sandbox/updater/TestPaths.java @@ -15,7 +15,7 @@ public static Path templateRepositoryPath() { } } - private static Path classpathFile(String name) throws URISyntaxException { + public static Path classpathFile(String name) throws URISyntaxException { URL resource = TestPaths.class.getResource(name); return new File(resource.toURI()).toPath(); } diff --git a/src/test/java/com/neo4j/sandbox/updater/UpdaterIT.java b/src/test/java/com/neo4j/sandbox/updater/UpdaterIT.java index 6363fa4..9f088ee 100644 --- a/src/test/java/com/neo4j/sandbox/updater/UpdaterIT.java +++ b/src/test/java/com/neo4j/sandbox/updater/UpdaterIT.java @@ -7,11 +7,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.io.File; import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import static com.neo4j.sandbox.updater.TestPaths.classpathFile; import static com.neo4j.sandbox.updater.TestPaths.templateRepositoryPath; import static org.assertj.core.api.Assertions.assertThat; @@ -22,11 +26,13 @@ class UpdaterIT { private Updater updater; @BeforeEach - void prepare(@TempDir Path tempDir) { + void prepare(@TempDir Path tempDir) throws Exception { sandboxCloneLocation = tempDir.resolve("northwind"); + Asciidoctor asciidoctor = Asciidoctor.Factory.create(); updater = new Updater( new FakeNorthwindGit(sandboxCloneLocation), - new MetadataReader(Asciidoctor.Factory.create()), + new MetadataReader(asciidoctor), + new BrowserGuideConverter(asciidoctor), new GithubSettings() ); } @@ -209,6 +215,15 @@ void updates_js_example() throws Exception { ); } + @Test + void generates_browser_guide_example() throws Exception { + Path fakeTwitterRepoLocation = classpathFile("/fake-twitter-repo"); + List paths = updater.generateBrowserGuides(fakeTwitterRepoLocation, "https://github.com/neo4j-graph-examples/twitter-v2"); + Path browserGuidePath = paths.iterator().next(); + assertThat(Files.readString(browserGuidePath, StandardCharsets.UTF_8)).contains("
    "); + assertThat(browserGuidePath.toFile().getName()).isEqualTo("twitter.neo4j-browser-guide"); + } + @Test void does_not_generate_graphql_sample_if_schema_is_absent() throws IOException { updater.updateCodeExamples(templateRepositoryPath(), sandboxCloneLocation, "https://github.com/neo4j-graph-examples/northwind"); @@ -223,4 +238,4 @@ void does_not_generate_graphql_sample_if_schema_is_absent() throws IOException { private static String absolutePathOf(Path path) { return path.toFile().getAbsolutePath(); } -} \ No newline at end of file +} diff --git a/src/test/resources/fake-template-repo/documentation/northwind.adoc b/src/test/resources/fake-template-repo/documentation/northwind.adoc new file mode 100644 index 0000000..c8b674a --- /dev/null +++ b/src/test/resources/fake-template-repo/documentation/northwind.adoc @@ -0,0 +1,217 @@ += Northwind Graph +:img: img + +== Northwind Graph + +From RDBMS to Graph, using a classic dataset + +The _Northwind Graph_ demonstrates how to migrate from a relational database to Neo4j. +The transformation is iterative and deliberate, emphasizing the conceptual shift from relational tables to the nodes and relationships of a graph. + +This guide will show you how to: + +* Load: create data from external CSV files +* Index: index nodes based on label +* Relate: transform foreign key references into data relationships +* Promote: transform join records into relationships + +''' + +== Product Catalog + +[.float-group] +-- +[.left] +image::{img}/product-category-supplier.png[] + +Northwind sells food products in a few categories, provided by suppliers. +Let's start by loading the product catalog tables. + +The load statements to the right require public internet access. `LOAD CSV` will retrieve a CSV file from a valid URL, applying a Cypher statement to each row using a named map (here we're using the name `row`). + +pass:[help cypher LOAD CSV] +-- + +=== Load records + +[source,cypher] +---- +LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/products.csv" AS row +CREATE (n:Product) +SET n = row, +n.unitPrice = toFloat(row.unitPrice), +n.unitsInStock = toInteger(row.unitsInStock), n.unitsOnOrder = toInteger(row.unitsOnOrder), +n.reorderLevel = toInteger(row.reorderLevel), n.discontinued = (row.discontinued <> "0"); +---- + +[source,cypher] +---- +LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/categories.csv" AS row +CREATE (n:Category) +SET n = row; +---- + +[source,cypher] +---- + LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/suppliers.csv" AS row + CREATE (n:Supplier) + SET n = row; +---- + +[source,cypher] +---- +CREATE INDEX ON :Product(productID); +CREATE INDEX ON :Category(categoryID); +CREATE INDEX ON :Supplier(supplierID); +---- + +''' + +== Product Catalog Graph + +[.left] +image::{img}/product-graph.png[] + +The products, categories and suppliers are related through foreign key references. +Let's promote those to data relationships to realize the graph. + +pass:[help cypher MATCH] + +=== Create data relationships + +Calculate join, materialize relationship. +See the http://neo4j.com/developer/guide-importing-data-and-etl[importing guide^] for more details. + +WARNING: Note you only need to compare property values like this when first creating relationships + +[source,cypher] +---- +MATCH (p:Product),(c:Category) +WHERE p.categoryID = c.categoryID +CREATE (p)-[:PART_OF]->(c); +---- + +[source,cypher] +---- +MATCH (p:Product),(s:Supplier) +WHERE p.supplierID = s.supplierID +CREATE (s)-[:SUPPLIES]->(p); +---- + +== Querying Product Catalog Graph + +[.left] +image::{img}/product-graph.png[] +Let's try some queries using patterns. + +pass:[help cypher MATCH] + +=== Query using patterns + +List the product categories provided by each supplier: + +[source,cypher] +---- +MATCH (s:Supplier)-->(:Product)-->(c:Category) +RETURN s.companyName as Company, collect(distinct c.categoryName) as Categories; +---- + +Find the produce suppliers: + +[source,cypher] +---- +MATCH (c:Category {categoryName:"Produce"})<--(:Product)<--(s:Supplier) +RETURN DISTINCT s.companyName as ProduceSuppliers; +---- + +== Customer Orders + +[.left] +image::{img}/customer-orders.png[] + +Northwind customers place orders which may detail multiple products. + +pass:[help cypher LOAD CSV] + +=== Load and index records + +[source,cypher] +---- +LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/customers.csv" AS row +CREATE (n:Customer) +SET n = row; +---- + +[source,cypher] +---- +LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/orders.csv" AS row +CREATE (n:Order) +SET n = row; +---- + +[source,cypher] +---- +CREATE INDEX ON :Customer(customerID); +CREATE INDEX ON :Order(orderID); +---- + +=== Create data relationships + +WARNING: Note you only need to compare property values like this when first creating relationships + +[source,cypher] +---- +MATCH (c:Customer),(o:Order) +WHERE c.customerID = o.customerID +CREATE (c)-[:PURCHASED]->(o); +---- + +== Customer Order Graph + +[.float-group] +-- +[.left] +image::{img}/order-graph.png[] + +Notice that Order Details are always part of an Order and that they{' '} _relate_ the Order to a Product — they're a join table. +Join tables are always a sign of a data relationship, indicating shared information between two other records. + +Here, we'll directly promote each OrderDetail record into a relationship in the graph. +-- + +pass:[help cypher LOAD CSV] + +=== Load and index records + +[source,cypher] +---- +LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/order-details.csv" AS row +MATCH (p:Product), (o:Order) +WHERE p.productID = row.productID AND o.orderID = row.orderID +CREATE (o)-[details:ORDERS]->(p) +SET details = row, details.quantity = toInteger(row.quantity); +---- + +WARNING: Note you only need to compare property values like this when first creating relationships + +=== Query using patterns + +[source,cypher] +---- +MATCH (cust:Customer)-[:PURCHASED]->(:Order)-[o:ORDERS]->(p:Product), + (p)-[:PART_OF]->(c:Category {categoryName:"Produce"}) +RETURN DISTINCT cust.contactName as CustomerName, SUM(o.quantity) AS TotalProductsPurchased; +---- + +== Next steps + +=== More code + +* pass:a[Movie Graph - actors & movies] +* pass:a[Cypher - query language fundamentals] + +=== References + +* https://neo4j.com/developer/guide-importing-data-and-etl/[Full Northwind import example^] +* https://neo4j.com/developer/[Developer resources^] +* https://neo4j.com/docs/cypher-manual[Neo4j Cypher Manual^] diff --git a/src/test/resources/fake-twitter-repo/documentation/twitter.adoc b/src/test/resources/fake-twitter-repo/documentation/twitter.adoc new file mode 100644 index 0000000..66c399e --- /dev/null +++ b/src/test/resources/fake-twitter-repo/documentation/twitter.adoc @@ -0,0 +1,236 @@ +== Twitter Graph + +Show data from your personal Twitter account + +_The Graph Your Network_ application inserts your Twitter activity into Neo4j. + +Here is a data model of the + +image:https://neo4jsandbox.com/guides/twitter/img/twitter-data-model.svg[image] + +image::https://guides.neo4j.com/sandbox/twitter/images/click-next.png[image] + +== Twitter Graph + +Show data from your personal Twitter account + +_The Graph Your Network_ application inserts your Twitter activity into Neo4j. + +This application allows you to query things like: + +. Who's mentioning you on Twitter +. Who are your most influential followers? +. What tags you use frequently +. How many people you follow also follow you back +. People tweeting about you, but you don't follow +. Links from interesting retweets +. Other people tweeting with some of your top hashtags + +== Twitter Graph + +== Your mentions + +To the right is a giant code block containing a single Cypher query statement to determine who's mentioning you on Twitter. + +. Click on the code blocks +. Notice they get copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +== Graph of some of your mentions + +[source,pre-scrollable,code,runnable] +---- +// Graph of some of your mentions +MATCH + (u:Me:User)-[p:POSTS]->(t:Tweet)-[:MENTIONS]->(m:User) +WITH + u,p,t,m, COUNT(m.screen_name) AS count +ORDER BY + count DESC +RETURN + u,p,t,m +LIMIT 10 +---- + +== Details as a table + +[source,pre-scrollable,code,runnable] +---- +// Detailed table of some of your mentions +MATCH + (u:User:Me)-[:POSTS]->(t:Tweet)-[:MENTIONS]->(m:User) +RETURN + m.screen_name AS screen_name, COUNT(m.screen_name) AS count +ORDER BY + count DESC +LIMIT 10 +---- + +== Twitter Graph + +== Most Influential Followers + +Who are your most influential followers? + +. Click on the code block +. Notice it gets copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +[source,pre-scrollable,code,runnable] +---- +// Most influential followers +MATCH + (follower:User)-[:FOLLOWS]->(u:User:Me) +RETURN + follower.screen_name AS user, follower.followers AS followers +ORDER BY + followers DESC +LIMIT 10 +---- + +== Twitter Graph + +== Most Tagged + +What hashtags have you used most often? + +. Click on the code block +. Notice it gets copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +[source,pre-scrollable,code,runnable] +---- +// The hashtags you have used most often +MATCH + (h:Hashtag)<-[:TAGS]-(t:Tweet)<-[:POSTS]-(u:User:Me) +WITH + h, COUNT(h) AS Hashtags +ORDER BY + Hashtags DESC +LIMIT 10 +RETURN + h.name, Hashtags +---- + +== Twitter Graph + +== Followback Rate + +At what rate do people you follow also follow you back? + +. Click on the code block +. Notice it gets copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +[source,pre-scrollable,code,runnable] +---- +// Followback rate +MATCH + (me:User:Me)-[:FOLLOWS]->(f) +WITH + me, f, size((f)-[:FOLLOWS]->(me)) as doesFollowBack +RETURN + SUM(doesFollowBack) / toFloat(COUNT(f)) AS followBackRate +---- + +== Twitter Graph + +== Follower Recommendations + +Who tweets about you, but you do not follow? + +. Click on the code block +. Notice it gets copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +[source,pre-scrollable,code,runnable] +---- +// Follower Recommendations - tweeting about you, but you don't follow +MATCH + (ou:User)-[:POSTS]->(t:Tweet)-[mt:MENTIONS]->(me:User:Me) +WITH + DISTINCT ou, me +WHERE + (ou)-[:FOLLOWS]->(me) + AND NOT + (me)-[:FOLLOWS]->(ou) +RETURN + ou.screen_name +---- + +== Twitter Graph + +== Links from interesting retweets + +What links do you retweet, and how often are they favorited? + +. Click on the code block +. Notice it gets copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +[source,pre-scrollable,code,runnable] +---- +// Links from interesting retweets +MATCH + (:User:Me)-[:POSTS]-> + (t:Tweet)-[:RETWEETS]->(rt)-[:CONTAINS]->(link:Link) +RETURN + t.id_str AS tweet, link.url AS url, rt.favorites AS favorites +ORDER BY + favorites DESC +LIMIT 10 +---- + +== Twitter Graph + +== Common Hashtags + +What users tweet with some of your top hashtags? + +. Click on the code block +. Notice it gets copied to the editor above ↑ +. Click the editor's play button to execute +. Wait for the query to finish + +[source,pre-scrollable,code,runnable] +---- +// Users tweeting with your top hashtags +MATCH + (me:User:Me)-[:POSTS]->(tweet:Tweet)-[:TAGS]->(ht) +MATCH + (ht)<-[:TAGS]-(tweet2:Tweet)<-[:POSTS]-(sugg:User) +WHERE + sugg <> me + AND NOT + (tweet2)-[:RETWEETS]->(tweet) +WITH + sugg, collect(distinct(ht)) as tags +RETURN + sugg.screen_name as friend, size(tags) as common +ORDER BY + common DESC +LIMIT 20 +---- + +== Next steps + +* Getting Started with Neo4j +* http://neo4j.com/download/[Download Neo4j] + +== More code + +* Movie Graph - Movies and actors +* Northwind Graph - from RDBMS to graph +* Query Templates - common ad-hoc queries +* Cypher - query language fundamentals + +== Reference + +* http://neo4j.com/developer[Developer resources] +* https://neo4j.com/docs/getting-started/current/[Neo4j Manual]