├── .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 | ![](https://raw.githubusercontent.com/NickTikhonov/sourcerer/master/screenshots/sourcerer.gif) 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 |
110 |

current community

111 |
112 | 168 |
169 |

170 | your communities

171 | 172 |
173 | 181 | 185 | 188 |
189 |
190 | 191 | 216 | 217 | 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 |
  1. Ask programming questions 343 |
  2. 344 |
  3. Answer and help your peers 345 |
  4. 346 |
  5. Get recognized for your expertise 347 |
  6. 348 |
349 |
350 |
351 |
352 | 390 |
391 | 394 |
395 | 396 | 397 | 398 |
399 | 400 | 404 |
405 |
406 | 407 | 423 | 424 | 490 | 491 | 492 | 493 | 494 | 648 |
408 | 409 | 410 |
411 | 412 | up vote 413 | 0 414 | down vote 415 | 416 | favorite 417 |
418 | 419 | 420 |
421 | 422 |
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 | 455 | 470 | 486 | 487 |
453 |
share|improve this question
454 |
456 | 471 | 485 |
488 |
489 |
495 |
496 | 497 | 502 | 503 | 504 | 505 | 506 | 521 | 530 | 531 | 532 | 547 | 556 | 557 | 558 | 573 | 583 | 584 | 585 | 600 | 609 | 610 | 611 | 626 | 635 | 636 | 637 |
507 | 508 | 509 | 510 | 514 | 517 | 518 | 519 |
511 | 1 513 | 515 |   516 |
520 |
522 |
523 | { -> }​​​​​ 524 | – Doorknob 527 | Nov 20 '15 at 23:39 528 |
529 |
533 | 534 | 535 | 536 | 540 | 543 | 544 | 545 |
537 | 2 539 | 541 |   542 |
546 |
548 |
549 | int main() instead of main(). Likewise add a return 0; statement after the printf line. 550 | – selbie 553 | Nov 20 '15 at 23:41 554 |
555 |
559 | 560 | 561 | 562 | 566 | 569 | 570 | 571 |
563 | 2 565 | 567 |   568 |
572 |
574 |
575 | Get yourself a better book. The ISO standard mandates that main must return an int. 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 |
582 |
586 | 587 | 588 | 589 | 593 | 596 | 597 | 598 |
590 | 1 592 | 594 |   595 |
599 |
601 |
602 | @ApproachingDarknessFish I'm using XCode and gcc (automatically included with XCode installation?) 603 | – pigletwithcurls 606 | Nov 20 '15 at 23:43 607 |
608 |
612 | 613 | 614 | 615 | 619 | 622 | 623 | 624 |
616 | 1 618 | 620 |   621 |
625 |
627 |
628 | The C Programming Language is a fantastic book, but it's really out of date. It hasn't had an update since C89. 629 | – user2357112 632 | Nov 20 '15 at 23:52 633 |
634 |
638 |
639 | 640 | 647 |
649 |
650 | 651 |
652 | 653 | 654 |
655 |
656 |

657 | 1 Answer 658 | 1 659 |

660 |
661 | 669 |
670 |
671 |
672 | 673 | 674 | 675 | 676 | 677 | 678 |
679 | 680 | 681 | 697 | 698 | 699 | 700 | 743 | 744 | 745 | 746 | 747 | 795 |
682 | 683 | 684 |
685 | 686 | up vote 687 | 3 688 | down vote 689 | 690 | 691 | 692 | accepted 693 | 694 |
695 | 696 |
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 | 719 | 720 | 721 | 722 | 740 | 741 |
718 |
share|improve this answer
723 | 724 | 725 | 739 |
742 |
748 |
749 | 750 | 755 | 756 | 757 | 758 | 759 | 773 | 782 | 783 | 784 |
760 | 761 | 762 | 763 | 766 | 769 | 770 | 771 |
764 |    765 | 767 |   768 |
772 |
774 |
775 | thank you very much :) 776 | – pigletwithcurls 779 | Nov 20 '15 at 23:51 780 |
781 |
785 |
786 | 787 | 794 |
796 |
797 | 798 |
799 | 800 | 801 | 802 |

Your Answer

803 | 804 | 805 | 814 | 815 | 816 | 848 | 849 | 850 |
851 | 852 |
853 |
854 |
855 | 856 |
857 |
858 | 859 |
 
860 | 861 | 862 | 863 | 864 | 865 | 866 |
867 |
868 |
869 | 870 | 871 |
872 | 873 | 874 | 875 |
876 |
877 | 878 | 924 | 931 | 951 | 952 |
953 | 954 |
955 | 956 | discard 957 | 958 |

959 | By posting your answer, you agree to the privacy policy and terms of service.

960 |
961 |
962 | 963 | 964 | 965 |

966 | Not the answer you're looking for? Browse other questions tagged or ask your own question.

967 |
968 |
969 | 1245 | 1246 | 1255 |
1258 | 1259 | 1260 |
1261 |
1262 | 1412 | 1415 | 1416 | 1428 | 1429 | 1453 | 1454 | 1455 | 1456 | 1457 | --------------------------------------------------------------------------------