├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── keymaps
└── sourcerer.cson
├── lib
├── answer-section.coffee
├── answer-selector.coffee
├── answer.coffee
├── page-loader.coffee
├── results-view.coffee
├── scraper.coffee
├── search.coffee
└── sourcerer.coffee
├── menus
└── sourcerer.cson
├── package.json
├── screenshots
└── sourcerer.gif
└── spec
├── answer-section-spec.coffee
├── answer-selector-spec.coffee
├── answer-spec.coffee
├── page-loader-spec.coffee
├── scraper-spec.coffee
└── so-one-accepted.html
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.0 - First Release
2 | * Every feature added
3 | * Every bug fixed
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Nick Tikhonov
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Sourcerer
2 | =========================
3 |
4 | Atom plugin for quickly finding and using StackOverflow code snippets.
5 |
6 | 
7 |
8 | Find and use code snippets relevant to your problem. Replace routine StackOverflow searching with a simple interface for searching and browsing the largest code snippet collection on the internet :)
9 |
10 | ## Usage
11 |
12 | * [Install Sourcerer](https://atom.io/packages/Sourcerer)
13 | * Write and select a problem description, e.g. 'fizzbuzz implementation' or 'quicksort'
14 | * Hit __alt-s__
15 | * View and insert relevant StackOverflow code snippets!
16 |
17 | ## Features
18 | * Searches stackoverflow questions using Google
19 | * Scrapes pages and filters accepted answers + those with a large number of votes
20 | * "I'm feeling lucky" mode - automatically insert the best found answer
21 | * Inserts StackOverflow answer text as comments (for context)
22 | * Automatically cites inserted snippets with the author's username
23 |
--------------------------------------------------------------------------------
/keymaps/sourcerer.cson:
--------------------------------------------------------------------------------
1 | # Keybindings require three things to be fully defined: A selector that is
2 | # matched against the focused element, the keystroke and the command to
3 | # execute.
4 | #
5 | # Below is a basic keybinding which registers on all platforms by applying to
6 | # the root workspace element.
7 |
8 | # For more detailed documentation see
9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth
10 | 'atom-workspace':
11 | 'alt-s': 'sourcerer:fetch'
12 |
--------------------------------------------------------------------------------
/lib/answer-section.coffee:
--------------------------------------------------------------------------------
1 | class AnswerSection
2 | constructor: (@type, @body) ->
3 | if @type not in ["text", "code"]
4 | throw new Error("Illegal type")
5 | else if @body is ""
6 | throw new Error("Illegal body size")
7 |
8 | isText: -> @type is "text"
9 | isCode: -> @type is "code"
10 |
11 | module.exports = AnswerSection
12 |
--------------------------------------------------------------------------------
/lib/answer-selector.coffee:
--------------------------------------------------------------------------------
1 | class AnswerSelector
2 | constructor: (@scraper) ->
3 | console.log "Constructed."
4 |
5 | find: (urls, {numAnswers, minVotes}) ->
6 | console.log "find called"
7 | self = this
8 | return new Promise (resolve, reject) ->
9 | foundAnswers = []
10 | findAnswersRecursive = ->
11 | if urls.length is 0
12 | if foundAnswers.length is 0
13 | reject reason: "Couldn't find any relevant answers"
14 | else
15 | resolve foundAnswers
16 | else
17 | currentLink = urls.shift()
18 | self.scraper.scrape(currentLink).then (newAnswers) ->
19 | foundAnswers = foundAnswers.concat self.__filter(newAnswers, minVotes)
20 | if foundAnswers.length >= numAnswers
21 | resolve self.__sort(foundAnswers).slice(0,numAnswers)
22 | else
23 | findAnswersRecursive()
24 | .catch (err) ->
25 | findAnswersRecursive()
26 |
27 | findAnswersRecursive()
28 |
29 | __sort: (answers) ->
30 | answers.sort (a, b) -> b.votes - a.votes
31 |
32 | __filter: (answers, minVotes) ->
33 | answers.filter (answer) -> answer.accepted or answer.votes > minVotes
34 |
35 | module.exports = AnswerSelector
36 |
--------------------------------------------------------------------------------
/lib/answer.coffee:
--------------------------------------------------------------------------------
1 | class Answer
2 | constructor: (@author, @votes, @accepted, @sections) ->
3 |
4 | hasCode: -> (@sections.filter (section) -> section.isCode()).length > 0
5 |
6 | insertWith: (editor, {insertDescription, credit}) ->
7 | if insertDescription
8 | @__insertComment editor, "~ Snippet by StackOverflow user #{@author} from an answer with #{@votes} votes. ~"
9 |
10 | for section in @sections
11 | if section.isCode()
12 | editor.insertText "\n" + section.body + "\n"
13 | else if section.isText()
14 | if insertDescription
15 | @__insertComment editor, section.body
16 |
17 | __insertComment: (editor, comment) ->
18 | editor.insertText comment + "\n", select: true
19 | selection = editor.getLastSelection()
20 | selection.toggleLineComments()
21 | selection.clear()
22 |
23 | module.exports = Answer
24 |
--------------------------------------------------------------------------------
/lib/page-loader.coffee:
--------------------------------------------------------------------------------
1 | request = require "request"
2 |
3 | class PageLoader
4 | # Downloads StackOverflow pages
5 |
6 | get: (url) ->
7 | allowedDomain = "http://www.stackoverflow.com/questions"
8 | return new Promise (resolve, reject) ->
9 | if url.indexOf allowedDomain is not -1
10 | request url, (error, response, body) ->
11 | if not error and response.statusCode is 200
12 | resolve body
13 | else
14 | reject reason: "Could not download the page"
15 | else
16 | reject reason: "Illegal URL"
17 |
18 |
19 |
20 | module.exports = PageLoader
21 |
--------------------------------------------------------------------------------
/lib/results-view.coffee:
--------------------------------------------------------------------------------
1 | {$$, SelectListView} = require 'atom-space-pen-views'
2 |
3 | class ResultView extends SelectListView
4 | # items is a list of string code snippets
5 | initialize: (@editor, items) ->
6 | super
7 |
8 | console.log "Provided to view:"
9 | console.log items
10 | @addClass('overlay from-top')
11 | @setItems(items)
12 | @panel ?= atom.workspace.addModalPanel(item: this)
13 | @panel.show()
14 | @focusFilterEditor()
15 |
16 | viewForItem: (item) ->
17 | $$ ->
18 | @li class: 'two-lines', =>
19 | @div "#{item.author} | #{item.votes} vote#{if item.votes is 1 then '' else 's'}", class: 'primary-line'
20 | @div class: 'secondary-line', =>
21 | for section in item.sections
22 | if section.type == "code"
23 | @pre section.body
24 | else if section.type == "text"
25 | @p section.body
26 |
27 | confirmed: (selectedAnswer) ->
28 | @cancel()
29 | selectedAnswer.insertWith @editor,
30 | insertDescription: atom.config.get('sourcerer.insertDescription'),
31 | credit: true
32 |
33 | cancelled: ->
34 | console.log("CANCELLED")
35 | @hide()
36 |
37 | hide: ->
38 | @panel?.hide()
39 |
40 | module.exports = ResultView
41 |
--------------------------------------------------------------------------------
/lib/scraper.coffee:
--------------------------------------------------------------------------------
1 | cheerio = require 'cheerio'
2 | AnswerSection = require './answer-section'
3 | Answer = require './answer'
4 |
5 | class Scraper
6 | constructor: (@loader) ->
7 |
8 | scrape: (url) ->
9 | self = this
10 | return new Promise (resolve, reject) ->
11 | self.loader.get(url).then (body) ->
12 | $ = cheerio.load body
13 |
14 | answers = []
15 | $('div.answer').each (i, elem) ->
16 | answers.push self.__scrapeAnswer(elem)
17 |
18 | resolve answers
19 | .catch reject
20 |
21 | __scrapeAnswer: (elem) ->
22 | sections = []
23 | $ = cheerio.load elem
24 | $('.post-text').children().each (i, child) ->
25 | if child.tagName == "pre"
26 | sections.push new AnswerSection("code", $(child).text())
27 | else if child.tagName == "p"
28 | sections.push new AnswerSection("text", $(child).text())
29 |
30 | author = $('.user-details a').text().trim()
31 | votes = parseInt $('.vote-count-post').text(), 10
32 | accepted = $('span.vote-accepted-on').length == 1
33 |
34 | return new Answer(author, votes, accepted, sections)
35 |
36 | module.exports = Scraper
37 |
--------------------------------------------------------------------------------
/lib/search.coffee:
--------------------------------------------------------------------------------
1 | google = require 'google'
2 | google.resultsPerPage = 10
3 |
4 | class Search
5 | buildSearchString: (query, language) ->
6 | query = query.toLowerCase().trim()
7 | "#{query} in #{language} site:stackoverflow.com"
8 |
9 | searchGoogle: (query, language) ->
10 | self = this
11 | return new Promise (resolve, reject) ->
12 | searchQuery = self.buildSearchString(query, language)
13 | google searchQuery, (err, res) ->
14 | if err
15 | reject reason: "Google Error - are you online?"
16 | else if res.links.length == 0
17 | reject reason: "No results were found"
18 | else
19 | resolve res.links.map (item) -> item.link
20 |
21 | module.exports = Search
22 |
--------------------------------------------------------------------------------
/lib/sourcerer.coffee:
--------------------------------------------------------------------------------
1 | {CompositeDisposable} = require 'atom'
2 |
3 | SearchEngine = require './search'
4 | PageLoader = require './page-loader'
5 | Scraper = require './scraper'
6 | AnswerSelector = require './answer-selector'
7 | ResultView = require './results-view'
8 |
9 | search = new SearchEngine()
10 | loader = new PageLoader()
11 | scraper = new Scraper(loader)
12 | selector = new AnswerSelector(scraper)
13 |
14 | module.exports =
15 | subscriptions: null
16 |
17 | config:
18 | minVotes:
19 | title: "Minimum Number of Votes"
20 | description: "The number of votes needed by an unaccepted answer to appear in the results."
21 | type: 'integer'
22 | default: 50
23 | minimum: 1
24 | numAnswers:
25 | title: "Minimum number of snippets"
26 | description: "Number of snippets fetched by Sourcerer per query"
27 | type: 'integer'
28 | default: 3
29 | minimum: 1
30 | luckyMode:
31 | title: "I'm feeling lucky"
32 | description: "Do not show the preview window, automatically insert the best snippet found based on the number of votes"
33 | type: 'boolean'
34 | default: false
35 | insertDescription:
36 | title: "Insert accompanying text"
37 | description: "Insert the accompanying StackOverflow answer text as well as the code"
38 | type: 'boolean'
39 | default: true
40 |
41 | activate: ->
42 | @subscriptions = new CompositeDisposable
43 | @subscriptions.add atom.commands.add 'atom-workspace',
44 | 'sourcerer:fetch': => @fetch()
45 |
46 | deactivate: ->
47 | @subscriptions.dispose()
48 |
49 | fetch: ->
50 | return unless editor = atom.workspace.getActiveTextEditor()
51 |
52 | language = editor.getGrammar().name
53 | query = editor.getSelectedText()
54 | if query.length == 0
55 | atom.notifications.addWarning "Please make a valid selection"
56 | return
57 |
58 | search.searchGoogle query, language
59 | .then (soLinks) ->
60 | atom.notifications.addSuccess "Googled problem."
61 | console.log selector
62 | return selector.find soLinks,
63 | numAnswers: atom.config.get('sourcerer.numAnswers')
64 | minVotes: atom.config.get('sourcerer.minVotes')
65 | .then (answers) ->
66 | if atom.config.get('sourcerer.luckyMode')
67 | best = answers[0]
68 | best.insertWith editor,
69 | insertDescription: atom.config.get('sourcerer.insertDescription'),
70 | credit: true
71 | else
72 | new ResultView(editor, answers)
73 | .catch displayErrorNotification
74 |
75 | # -- end of module.exports --
76 |
77 | displayErrorNotification = (err) ->
78 | atom.notifications.addError err.reason
79 |
--------------------------------------------------------------------------------
/menus/sourcerer.cson:
--------------------------------------------------------------------------------
1 | 'context-menu':
2 | 'atom-text-editor': [
3 | {
4 | 'label': 'Sourcerer: Find source code for selection'
5 | 'command': 'sourcerer:fetch'
6 | }
7 | ]
8 | 'menu': [
9 | {
10 | 'label': 'Packages'
11 | 'submenu': [
12 | 'label': 'Sourcerer'
13 | 'submenu': [
14 | {
15 | 'label': 'Find source code for selection'
16 | 'command': 'sourcerer:fetch'
17 | }
18 | ]
19 | ]
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sourcerer",
3 | "main": "./lib/sourcerer",
4 | "version": "2.0.0",
5 | "description": "Find StackOverflow code snippets relevant to your problem with alt-s.",
6 | "keywords": [
7 | "StackOverflow",
8 | "code",
9 | "snippets"
10 | ],
11 | "activationCommands": {
12 | "atom-workspace": "sourcerer:fetch"
13 | },
14 | "repository": "https://github.com/nicktikhonov/sourcerer",
15 | "license": "MIT",
16 | "engines": {
17 | "atom": ">=1.0.0 <2.0.0"
18 | },
19 | "dependencies": {
20 | "atom-space-pen-views": "^2.0.3",
21 | "cheerio": "^0.20.0",
22 | "google": ">=2.0.0",
23 | "request": "^2.72.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/screenshots/sourcerer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NickTikhonov/sourcerer/061726868b1ed5f83d88b991f0a92e4efd6c8d02/screenshots/sourcerer.gif
--------------------------------------------------------------------------------
/spec/answer-section-spec.coffee:
--------------------------------------------------------------------------------
1 | AnswerSection = require '../lib/answer-section'
2 |
3 | describe "AnswerSection", ->
4 | it "can be a code section", ->
5 | section = new AnswerSection("code", "print 'Hello World'\n")
6 | expect(section.isCode()).toBe(true)
7 |
8 | it "can be a text section", ->
9 | section = new AnswerSection("text", "Example text section")
10 | expect(section.isText()).toBe(true)
11 |
12 | it "cannot be an unsupported type", ->
13 | expect(-> new AnswerSection("invalid", "some value")).toThrow(new Error("Illegal type"));
14 |
15 | it "cannot have an empty body", ->
16 | expect(-> new AnswerSection("text", "")).toThrow(new Error("Illegal body size"))
17 |
--------------------------------------------------------------------------------
/spec/answer-selector-spec.coffee:
--------------------------------------------------------------------------------
1 | AnswerSelector = require '../lib/answer-selector'
2 | Answer = require '../lib/answer'
3 | AnswerSection = require '../lib/answer-section'
4 |
5 | SAMPLE_URL_1 = "http://www.stackoverflow.com/questions/sample1"
6 | SAMPLE_URL_2 = "http://www.stackoverflow.com/questions/sample2"
7 | SAMPLE_URL_3 = "http://www.stackoverflow.com/questions/sample3"
8 |
9 | describe "AnswerSelector", ->
10 |
11 | it "should find the correct number of answers", ->
12 | scraper =
13 | scrape: ->
14 |
15 | spyOn(scraper, 'scrape').andReturn new Promise (resolve, reject) ->
16 | resolve [
17 | new Answer("bob", 10, true, [new AnswerSection("text", "sample")])
18 | new Answer("bob", 10, true, [new AnswerSection("text", "sample")])
19 | new Answer("bob", 10, true, [new AnswerSection("text", "sample")])
20 | ]
21 |
22 | selector = new AnswerSelector(scraper)
23 |
24 | waitsForPromise ->
25 | selector.find([SAMPLE_URL_1], numAnswers: 2, minVotes: 5).then (answers) ->
26 | expect(answers).toEqual [
27 | new Answer("bob", 10, true, [new AnswerSection("text", "sample")])
28 | new Answer("bob", 10, true, [new AnswerSection("text", "sample")])
29 | ]
30 |
31 | it "should find answers by scraping multiple urls", ->
32 | scraper =
33 | scrape: ->
34 |
35 | spyOn(scraper, 'scrape').andCallFake (url) ->
36 | return new Promise (resolve, reject) ->
37 | resolve [
38 | [
39 | new Answer("bob", 10, true, [new AnswerSection("text", "answer1")])
40 | new Answer("bob", 10, true, [new AnswerSection("text", "answer2")])
41 | new Answer("bob", 10, true, [new AnswerSection("text", "answer3")])
42 | ],
43 | [
44 | new Answer("bob", 10, true, [new AnswerSection("text", "answer4")])
45 | new Answer("bob", 10, true, [new AnswerSection("text", "answer5")])
46 | new Answer("bob", 10, true, [new AnswerSection("text", "answer6")])
47 | ]
48 | ][scraper.scrape.callCount - 1]
49 |
50 | selector = new AnswerSelector(scraper)
51 |
52 | waitsForPromise ->
53 | selector.find([SAMPLE_URL_1, SAMPLE_URL_2], numAnswers: 4, minVotes: 5).then (answers) ->
54 | expect(answers).toEqual [
55 | new Answer("bob", 10, true, [new AnswerSection("text", "answer1")])
56 | new Answer("bob", 10, true, [new AnswerSection("text", "answer2")])
57 | new Answer("bob", 10, true, [new AnswerSection("text", "answer3")])
58 | new Answer("bob", 10, true, [new AnswerSection("text", "answer4")])
59 | ]
60 |
61 | it "should stop scraping when it found enough answers", ->
62 | scraper =
63 | scrape: ->
64 |
65 | spyOn(scraper, 'scrape').andCallFake (url) ->
66 | return new Promise (resolve, reject) ->
67 | resolve [
68 | [
69 | new Answer("bob", 10, true, [new AnswerSection("text", "answer1")])
70 | new Answer("bob", 10, true, [new AnswerSection("text", "answer2")])
71 | new Answer("bob", 10, true, [new AnswerSection("text", "answer3")])
72 | ],
73 | [
74 | new Answer("bob", 10, true, [new AnswerSection("text", "answer4")])
75 | new Answer("bob", 10, true, [new AnswerSection("text", "answer5")])
76 | new Answer("bob", 10, true, [new AnswerSection("text", "answer6")])
77 | ],
78 | [
79 | new Answer("bob", 10, true, [new AnswerSection("text", "answer7")])
80 | new Answer("bob", 10, true, [new AnswerSection("text", "answer8")])
81 | new Answer("bob", 10, true, [new AnswerSection("text", "answer9")])
82 | ]
83 | ][scraper.scrape.callCount - 1]
84 |
85 | selector = new AnswerSelector(scraper)
86 |
87 | waitsForPromise ->
88 | selector.find([SAMPLE_URL_1, SAMPLE_URL_2, SAMPLE_URL_3], numAnswers: 4, minVotes: 5).then (answers) ->
89 | expect(scraper.scrape.callCount).toBe(2)
90 |
91 |
92 | it "should sort answers by the number of votes", ->
93 | scraper =
94 | scrape: ->
95 |
96 | spyOn(scraper, 'scrape').andReturn new Promise (resolve, reject) ->
97 | resolve [
98 | new Answer("bob", 1, true, [new AnswerSection("text", "answer1")])
99 | new Answer("bob", 7, true, [new AnswerSection("text", "answer2")])
100 | new Answer("bob", 5, true, [new AnswerSection("text", "answer3")])
101 | ]
102 |
103 | selector = new AnswerSelector(scraper)
104 |
105 | waitsForPromise ->
106 | selector.find([SAMPLE_URL_1], numAnswers: 3, minVotes: 5).then (answers) ->
107 | expect(answers).toEqual [
108 | new Answer("bob", 7, true, [new AnswerSection("text", "answer2")])
109 | new Answer("bob", 5, true, [new AnswerSection("text", "answer3")])
110 | new Answer("bob", 1, true, [new AnswerSection("text", "answer1")])
111 | ]
112 |
113 | it "should ignore unnacepted answers with few enough votes", ->
114 | scraper =
115 | scrape: ->
116 |
117 | spyOn(scraper, 'scrape').andReturn new Promise (resolve, reject) ->
118 | resolve [
119 | new Answer("bob", 1, true, [new AnswerSection("text", "answer1")])
120 | new Answer("bob", 9, false, [new AnswerSection("text", "answer2")])
121 | new Answer("bob", 9, false, [new AnswerSection("text", "answer3")])
122 | new Answer("bob", 9, true, [new AnswerSection("text", "answer3")])
123 | new Answer("bob", 5, false, [new AnswerSection("text", "answer3")])
124 | ]
125 |
126 | selector = new AnswerSelector(scraper)
127 |
128 | waitsForPromise ->
129 | selector.find([SAMPLE_URL_1], numAnswers: 4, minVotes: 6).then (answers) ->
130 | expect(answers).toEqual [
131 | new Answer("bob", 9, false, [new AnswerSection("text", "answer2")])
132 | new Answer("bob", 9, false, [new AnswerSection("text", "answer3")])
133 | new Answer("bob", 9, true, [new AnswerSection("text", "answer3")])
134 | new Answer("bob", 1, true, [new AnswerSection("text", "answer1")])
135 | ]
136 |
--------------------------------------------------------------------------------
/spec/answer-spec.coffee:
--------------------------------------------------------------------------------
1 | AnswerSection = require '../lib/answer-section'
2 | Answer= require '../lib/answer'
3 |
4 | describe "Answer", ->
5 | it "can have code", ->
6 | sections = []
7 | sections.push new AnswerSection("text", "This is how you print hello world")
8 | sections.push new AnswerSection("code", "print 'hello'")
9 | answer = new Answer("Bob", 30, true, sections)
10 |
11 | expect(answer.hasCode()).toBe(true)
12 |
13 | it "can have no code", ->
14 | sections = []
15 | sections.push new AnswerSection("text", "The first text section")
16 | sections.push new AnswerSection("text", "The second text section")
17 | answer = new Answer("Bob", 30, true, sections)
18 |
19 | expect(answer.hasCode()).toBe(false)
20 |
--------------------------------------------------------------------------------
/spec/page-loader-spec.coffee:
--------------------------------------------------------------------------------
1 | PageLoader = require('../lib/page-loader')
2 | loader = new PageLoader()
3 |
4 | describe "PageLoader", ->
5 | it "can download stackoverflow pages", ->
6 | url = "http://stackoverflow.com/questions/32473272/meteor-query-for-all-documents-with-unique-field"
7 | waitsForPromise ->
8 | loader.get(url).then (body) ->
9 | expect(typeof body).toEqual("string")
10 |
11 | it "cannot download non-stackoverflow pages", ->
12 | url = "http://www.google.com"
13 | waitsForPromise ->
14 | loader.get(url).catch (err) ->
15 | expect(err.reason).toEqual("Illegal URL")
16 |
--------------------------------------------------------------------------------
/spec/scraper-spec.coffee:
--------------------------------------------------------------------------------
1 | Scraper = require '../lib/scraper'
2 | PageLoader = require '../lib/page-loader'
3 | fs = require 'fs'
4 | path = require 'path'
5 |
6 | describe "Scraper", ->
7 | it "can first no answers", ->
8 | waitsForPromise ->
9 | mockLoader =
10 | get: (url) ->
11 | return new Promise (resolve, reject) ->
12 | resolve ""
13 |
14 | scraper = new Scraper(mockLoader)
15 | scraper.scrape('someurl').then (answers) ->
16 | expect(answers).toEqual([])
17 |
18 | it "can find one answer", ->
19 | waitsForPromise ->
20 | mockLoader =
21 | get: (url) ->
22 | return new Promise (resolve, reject) ->
23 | fs.readFile path.resolve(__dirname, 'so-one-accepted.html'), (err, body) ->
24 | resolve body
25 |
26 | new Scraper(mockLoader).scrape('someurl').then (answers) ->
27 | expect(answers.length).toBe(1)
28 |
29 | first = answers[0]
30 | expect(first.author).toBe("Yuriko")
31 | expect(first.votes).toBe(3)
32 | expect(first.accepted).toBe(true)
33 | expect(first.sections.length).toBe(3)
34 | expect(first.sections[0].isText()).toBe(true)
35 | expect(first.sections[1].isCode()).toBe(true)
36 | expect(first.sections[2].isText()).toBe(true)
37 |
--------------------------------------------------------------------------------
/spec/so-one-accepted.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Error in compiling hello world in c - Stack Overflow
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
63 |
64 |
65 |
72 |
73 |
82 |
83 |
84 |
85 |
86 |
87 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
112 |
168 |
173 |
181 |
185 |
188 |
189 |
190 |
191 |
216 |
217 |
218 |
219 |
220 |
277 |
278 |
279 |
280 |
283 |
284 |
285 |
286 |
287 |
288 |
291 |
292 |
293 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 | Stack Overflow is a community of 4.7 million programmers, just like you, helping each other.
334 |
335 |
Join them; it only takes a minute:
336 |
337 |
Sign up
338 |
339 |
340 |
Join the Stack Overflow community to:
341 |
342 | Ask programming questions
343 |
344 | Answer and help your peers
345 |
346 | Get recognized for your expertise
347 |
348 |
349 |
350 |
351 |
352 |
390 |
391 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
I am a beginner in learning c although I took two java courses at school. I just started learning c with "The C Programming" book.
429 |
430 |
I am trying to compile my first program, "hello.c"
431 |
432 |
I typed in as the book says:
433 |
434 |
#include <stdio.h>
435 |
436 | main()
437 | {
438 | printf("hello, world\n");
439 | }
440 |
441 |
442 |
However, it says that I have to write the type specifier 'int' before main().
443 | I'm trying to understand why this is so because the book dosen't indicate about the type specifier.
444 |
445 |
Thank you!
446 |
447 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 | asked Nov 20 '15 at 23:39
474 |
475 |
478 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
639 |
640 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
up vote
687 |
3
688 |
down vote
689 |
690 |
691 |
692 |
accepted
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
Your main
function needs to return something, that's what the compiler is telling you.
703 |
704 |
#include <stdio.h>
705 | #include <stdlib.h>
706 |
707 | int main() {
708 | printf("hello, world\n");
709 | return EXIT_SUCCESS;
710 | }
711 |
712 |
713 |
EXIT_SUCCESS
is defined in stdlib
. It means that the application successfully ended. Its value is usually 0.
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 | answered Nov 20 '15 at 23:42
728 |
729 |
732 |
733 |
Yuriko
734 |
735 | 439 12
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
786 |
787 |
794 |
795 |
796 |
797 |
798 |
962 |
963 |
964 |
965 |
966 | Not the answer you're looking for? Browse other questions tagged c or ask your own question .
967 |
968 |
969 |
1245 |
1246 |
1255 |
1256 |
1257 | lang-c
1258 |
1259 |
1260 |
1261 |
1262 |
1412 |
1413 | Stack Overflow works best with JavaScript enabled
1414 |
1415 |
1416 |
1428 |
1429 |
1453 |
1454 |
1455 |
1456 |
1457 |
--------------------------------------------------------------------------------
{
->}
524 | – Doorknob 527 | Nov 20 '15 at 23:39 528 |int main()
instead ofmain()
. Likewise add areturn 0;
statement after theprintf
line. 550 | – selbie 553 | Nov 20 '15 at 23:41 554 |main
must return anint
. Later versions of the standard remove the requirement to explicitly return zero but I think that was a mistake myself, and prefer to stay explicit :-) 576 | – paxdiablo 579 | Nov 20 '15 at 23:42 580 | 581 |