1package gofeed 2 3import ( 4 "fmt" 5 "strings" 6 "time" 7 8 "github.com/mmcdole/gofeed/atom" 9 ext "github.com/mmcdole/gofeed/extensions" 10 "github.com/mmcdole/gofeed/internal/shared" 11 "github.com/mmcdole/gofeed/rss" 12) 13 14// Translator converts a particular feed (atom.Feed or rss.Feed) 15// into the generic Feed struct 16type Translator interface { 17 Translate(feed interface{}) (*Feed, error) 18} 19 20// DefaultRSSTranslator converts an rss.Feed struct 21// into the generic Feed struct. 22// 23// This default implementation defines a set of 24// mapping rules between rss.Feed -> Feed 25// for each of the fields in Feed. 26type DefaultRSSTranslator struct{} 27 28// Translate converts an RSS feed into the universal 29// feed type. 30func (t *DefaultRSSTranslator) Translate(feed interface{}) (*Feed, error) { 31 rss, found := feed.(*rss.Feed) 32 if !found { 33 return nil, fmt.Errorf("Feed did not match expected type of *rss.Feed") 34 } 35 36 result := &Feed{} 37 result.Title = t.translateFeedTitle(rss) 38 result.Description = t.translateFeedDescription(rss) 39 result.Link = t.translateFeedLink(rss) 40 result.FeedLink = t.translateFeedFeedLink(rss) 41 result.Updated = t.translateFeedUpdated(rss) 42 result.UpdatedParsed = t.translateFeedUpdatedParsed(rss) 43 result.Published = t.translateFeedPublished(rss) 44 result.PublishedParsed = t.translateFeedPublishedParsed(rss) 45 result.Author = t.translateFeedAuthor(rss) 46 result.Language = t.translateFeedLanguage(rss) 47 result.Image = t.translateFeedImage(rss) 48 result.Copyright = t.translateFeedCopyright(rss) 49 result.Generator = t.translateFeedGenerator(rss) 50 result.Categories = t.translateFeedCategories(rss) 51 result.Items = t.translateFeedItems(rss) 52 result.ITunesExt = rss.ITunesExt 53 result.DublinCoreExt = rss.DublinCoreExt 54 result.Extensions = rss.Extensions 55 result.FeedVersion = rss.Version 56 result.FeedType = "rss" 57 return result, nil 58} 59 60func (t *DefaultRSSTranslator) translateFeedItem(rssItem *rss.Item) (item *Item) { 61 item = &Item{} 62 item.Title = t.translateItemTitle(rssItem) 63 item.Description = t.translateItemDescription(rssItem) 64 item.Content = t.translateItemContent(rssItem) 65 item.Link = t.translateItemLink(rssItem) 66 item.Published = t.translateItemPublished(rssItem) 67 item.PublishedParsed = t.translateItemPublishedParsed(rssItem) 68 item.Author = t.translateItemAuthor(rssItem) 69 item.GUID = t.translateItemGUID(rssItem) 70 item.Image = t.translateItemImage(rssItem) 71 item.Categories = t.translateItemCategories(rssItem) 72 item.Enclosures = t.translateItemEnclosures(rssItem) 73 item.DublinCoreExt = rssItem.DublinCoreExt 74 item.ITunesExt = rssItem.ITunesExt 75 item.Extensions = rssItem.Extensions 76 return 77} 78 79func (t *DefaultRSSTranslator) translateFeedTitle(rss *rss.Feed) (title string) { 80 if rss.Title != "" { 81 title = rss.Title 82 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Title != nil { 83 title = t.firstEntry(rss.DublinCoreExt.Title) 84 } 85 return 86} 87 88func (t *DefaultRSSTranslator) translateFeedDescription(rss *rss.Feed) (desc string) { 89 return rss.Description 90} 91 92func (t *DefaultRSSTranslator) translateFeedLink(rss *rss.Feed) (link string) { 93 if rss.Link != "" { 94 link = rss.Link 95 } else if rss.ITunesExt != nil && rss.ITunesExt.Subtitle != "" { 96 link = rss.ITunesExt.Subtitle 97 } 98 return 99} 100 101func (t *DefaultRSSTranslator) translateFeedFeedLink(rss *rss.Feed) (link string) { 102 atomExtensions := t.extensionsForKeys([]string{"atom", "atom10", "atom03"}, rss.Extensions) 103 for _, ex := range atomExtensions { 104 if links, ok := ex["link"]; ok { 105 for _, l := range links { 106 if l.Attrs["Rel"] == "self" { 107 link = l.Value 108 } 109 } 110 } 111 } 112 return 113} 114 115func (t *DefaultRSSTranslator) translateFeedUpdated(rss *rss.Feed) (updated string) { 116 if rss.LastBuildDate != "" { 117 updated = rss.LastBuildDate 118 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Date != nil { 119 updated = t.firstEntry(rss.DublinCoreExt.Date) 120 } 121 return 122} 123 124func (t *DefaultRSSTranslator) translateFeedUpdatedParsed(rss *rss.Feed) (updated *time.Time) { 125 if rss.LastBuildDateParsed != nil { 126 updated = rss.LastBuildDateParsed 127 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Date != nil { 128 dateText := t.firstEntry(rss.DublinCoreExt.Date) 129 date, err := shared.ParseDate(dateText) 130 if err == nil { 131 updated = &date 132 } 133 } 134 return 135} 136 137func (t *DefaultRSSTranslator) translateFeedPublished(rss *rss.Feed) (published string) { 138 return rss.PubDate 139} 140 141func (t *DefaultRSSTranslator) translateFeedPublishedParsed(rss *rss.Feed) (published *time.Time) { 142 return rss.PubDateParsed 143} 144 145func (t *DefaultRSSTranslator) translateFeedAuthor(rss *rss.Feed) (author *Person) { 146 if rss.ManagingEditor != "" { 147 name, address := shared.ParseNameAddress(rss.ManagingEditor) 148 author = &Person{} 149 author.Name = name 150 author.Email = address 151 } else if rss.WebMaster != "" { 152 name, address := shared.ParseNameAddress(rss.WebMaster) 153 author = &Person{} 154 author.Name = name 155 author.Email = address 156 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Author != nil { 157 dcAuthor := t.firstEntry(rss.DublinCoreExt.Author) 158 name, address := shared.ParseNameAddress(dcAuthor) 159 author = &Person{} 160 author.Name = name 161 author.Email = address 162 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Creator != nil { 163 dcCreator := t.firstEntry(rss.DublinCoreExt.Creator) 164 name, address := shared.ParseNameAddress(dcCreator) 165 author = &Person{} 166 author.Name = name 167 author.Email = address 168 } else if rss.ITunesExt != nil && rss.ITunesExt.Author != "" { 169 name, address := shared.ParseNameAddress(rss.ITunesExt.Author) 170 author = &Person{} 171 author.Name = name 172 author.Email = address 173 } 174 return 175} 176 177func (t *DefaultRSSTranslator) translateFeedLanguage(rss *rss.Feed) (language string) { 178 if rss.Language != "" { 179 language = rss.Language 180 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Language != nil { 181 language = t.firstEntry(rss.DublinCoreExt.Language) 182 } 183 return 184} 185 186func (t *DefaultRSSTranslator) translateFeedImage(rss *rss.Feed) (image *Image) { 187 if rss.Image != nil { 188 image = &Image{} 189 image.Title = rss.Image.Title 190 image.URL = rss.Image.URL 191 } else if rss.ITunesExt != nil && rss.ITunesExt.Image != "" { 192 image = &Image{} 193 image.URL = rss.ITunesExt.Image 194 } 195 return 196} 197 198func (t *DefaultRSSTranslator) translateFeedCopyright(rss *rss.Feed) (rights string) { 199 if rss.Copyright != "" { 200 rights = rss.Copyright 201 } else if rss.DublinCoreExt != nil && rss.DublinCoreExt.Rights != nil { 202 rights = t.firstEntry(rss.DublinCoreExt.Rights) 203 } 204 return 205} 206 207func (t *DefaultRSSTranslator) translateFeedGenerator(rss *rss.Feed) (generator string) { 208 return rss.Generator 209} 210 211func (t *DefaultRSSTranslator) translateFeedCategories(rss *rss.Feed) (categories []string) { 212 cats := []string{} 213 if rss.Categories != nil { 214 for _, c := range rss.Categories { 215 cats = append(cats, c.Value) 216 } 217 } 218 219 if rss.ITunesExt != nil && rss.ITunesExt.Keywords != "" { 220 keywords := strings.Split(rss.ITunesExt.Keywords, ",") 221 for _, k := range keywords { 222 cats = append(cats, k) 223 } 224 } 225 226 if rss.ITunesExt != nil && rss.ITunesExt.Categories != nil { 227 for _, c := range rss.ITunesExt.Categories { 228 cats = append(cats, c.Text) 229 if c.Subcategory != nil { 230 cats = append(cats, c.Subcategory.Text) 231 } 232 } 233 } 234 235 if rss.DublinCoreExt != nil && rss.DublinCoreExt.Subject != nil { 236 for _, c := range rss.DublinCoreExt.Subject { 237 cats = append(cats, c) 238 } 239 } 240 241 if len(cats) > 0 { 242 categories = cats 243 } 244 245 return 246} 247 248func (t *DefaultRSSTranslator) translateFeedItems(rss *rss.Feed) (items []*Item) { 249 items = []*Item{} 250 for _, i := range rss.Items { 251 items = append(items, t.translateFeedItem(i)) 252 } 253 return 254} 255 256func (t *DefaultRSSTranslator) translateItemTitle(rssItem *rss.Item) (title string) { 257 if rssItem.Title != "" { 258 title = rssItem.Title 259 } else if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Title != nil { 260 title = t.firstEntry(rssItem.DublinCoreExt.Title) 261 } 262 return 263} 264 265func (t *DefaultRSSTranslator) translateItemDescription(rssItem *rss.Item) (desc string) { 266 if rssItem.Description != "" { 267 desc = rssItem.Description 268 } else if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Description != nil { 269 desc = t.firstEntry(rssItem.DublinCoreExt.Description) 270 } 271 return 272} 273 274func (t *DefaultRSSTranslator) translateItemContent(rssItem *rss.Item) (content string) { 275 return rssItem.Content 276} 277 278func (t *DefaultRSSTranslator) translateItemLink(rssItem *rss.Item) (link string) { 279 return rssItem.Link 280} 281 282func (t *DefaultRSSTranslator) translateItemUpdated(rssItem *rss.Item) (updated string) { 283 if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Date != nil { 284 updated = t.firstEntry(rssItem.DublinCoreExt.Date) 285 } 286 return updated 287} 288 289func (t *DefaultRSSTranslator) translateItemUpdatedParsed(rssItem *rss.Item) (updated *time.Time) { 290 if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Date != nil { 291 updatedText := t.firstEntry(rssItem.DublinCoreExt.Date) 292 updatedDate, err := shared.ParseDate(updatedText) 293 if err == nil { 294 updated = &updatedDate 295 } 296 } 297 return 298} 299 300func (t *DefaultRSSTranslator) translateItemPublished(rssItem *rss.Item) (pubDate string) { 301 if rssItem.PubDate != "" { 302 return rssItem.PubDate 303 } else if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Date != nil { 304 return t.firstEntry(rssItem.DublinCoreExt.Date) 305 } 306 return 307} 308 309func (t *DefaultRSSTranslator) translateItemPublishedParsed(rssItem *rss.Item) (pubDate *time.Time) { 310 if rssItem.PubDateParsed != nil { 311 return rssItem.PubDateParsed 312 } else if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Date != nil { 313 pubDateText := t.firstEntry(rssItem.DublinCoreExt.Date) 314 pubDateParsed, err := shared.ParseDate(pubDateText) 315 if err == nil { 316 pubDate = &pubDateParsed 317 } 318 } 319 return 320} 321 322func (t *DefaultRSSTranslator) translateItemAuthor(rssItem *rss.Item) (author *Person) { 323 if rssItem.Author != "" { 324 name, address := shared.ParseNameAddress(rssItem.Author) 325 author = &Person{} 326 author.Name = name 327 author.Email = address 328 } else if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Author != nil { 329 dcAuthor := t.firstEntry(rssItem.DublinCoreExt.Author) 330 name, address := shared.ParseNameAddress(dcAuthor) 331 author = &Person{} 332 author.Name = name 333 author.Email = address 334 } else if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Creator != nil { 335 dcCreator := t.firstEntry(rssItem.DublinCoreExt.Creator) 336 name, address := shared.ParseNameAddress(dcCreator) 337 author = &Person{} 338 author.Name = name 339 author.Email = address 340 } else if rssItem.ITunesExt != nil && rssItem.ITunesExt.Author != "" { 341 name, address := shared.ParseNameAddress(rssItem.ITunesExt.Author) 342 author = &Person{} 343 author.Name = name 344 author.Email = address 345 } 346 return 347} 348 349func (t *DefaultRSSTranslator) translateItemGUID(rssItem *rss.Item) (guid string) { 350 if rssItem.GUID != nil { 351 guid = rssItem.GUID.Value 352 } 353 return 354} 355 356func (t *DefaultRSSTranslator) translateItemImage(rssItem *rss.Item) (image *Image) { 357 if rssItem.ITunesExt != nil && rssItem.ITunesExt.Image != "" { 358 image = &Image{} 359 image.URL = rssItem.ITunesExt.Image 360 } 361 return 362} 363 364func (t *DefaultRSSTranslator) translateItemCategories(rssItem *rss.Item) (categories []string) { 365 cats := []string{} 366 if rssItem.Categories != nil { 367 for _, c := range rssItem.Categories { 368 cats = append(cats, c.Value) 369 } 370 } 371 372 if rssItem.ITunesExt != nil && rssItem.ITunesExt.Keywords != "" { 373 keywords := strings.Split(rssItem.ITunesExt.Keywords, ",") 374 for _, k := range keywords { 375 cats = append(cats, k) 376 } 377 } 378 379 if rssItem.DublinCoreExt != nil && rssItem.DublinCoreExt.Subject != nil { 380 for _, c := range rssItem.DublinCoreExt.Subject { 381 cats = append(cats, c) 382 } 383 } 384 385 if len(cats) > 0 { 386 categories = cats 387 } 388 389 return 390} 391 392func (t *DefaultRSSTranslator) translateItemEnclosures(rssItem *rss.Item) (enclosures []*Enclosure) { 393 if rssItem.Enclosure != nil { 394 e := &Enclosure{} 395 e.URL = rssItem.Enclosure.URL 396 e.Type = rssItem.Enclosure.Type 397 e.Length = rssItem.Enclosure.Length 398 enclosures = []*Enclosure{e} 399 } 400 return 401} 402 403func (t *DefaultRSSTranslator) extensionsForKeys(keys []string, extensions ext.Extensions) (matches []map[string][]ext.Extension) { 404 matches = []map[string][]ext.Extension{} 405 406 if extensions == nil { 407 return 408 } 409 410 for _, key := range keys { 411 if match, ok := extensions[key]; ok { 412 matches = append(matches, match) 413 } 414 } 415 return 416} 417 418func (t *DefaultRSSTranslator) firstEntry(entries []string) (value string) { 419 if entries == nil { 420 return 421 } 422 423 if len(entries) == 0 { 424 return 425 } 426 427 return entries[0] 428} 429 430// DefaultAtomTranslator converts an atom.Feed struct 431// into the generic Feed struct. 432// 433// This default implementation defines a set of 434// mapping rules between atom.Feed -> Feed 435// for each of the fields in Feed. 436type DefaultAtomTranslator struct{} 437 438// Translate converts an Atom feed into the universal 439// feed type. 440func (t *DefaultAtomTranslator) Translate(feed interface{}) (*Feed, error) { 441 atom, found := feed.(*atom.Feed) 442 if !found { 443 return nil, fmt.Errorf("Feed did not match expected type of *atom.Feed") 444 } 445 446 result := &Feed{} 447 result.Title = t.translateFeedTitle(atom) 448 result.Description = t.translateFeedDescription(atom) 449 result.Link = t.translateFeedLink(atom) 450 result.FeedLink = t.translateFeedFeedLink(atom) 451 result.Updated = t.translateFeedUpdated(atom) 452 result.UpdatedParsed = t.translateFeedUpdatedParsed(atom) 453 result.Author = t.translateFeedAuthor(atom) 454 result.Language = t.translateFeedLanguage(atom) 455 result.Image = t.translateFeedImage(atom) 456 result.Copyright = t.translateFeedCopyright(atom) 457 result.Categories = t.translateFeedCategories(atom) 458 result.Generator = t.translateFeedGenerator(atom) 459 result.Items = t.translateFeedItems(atom) 460 result.Extensions = atom.Extensions 461 result.FeedVersion = atom.Version 462 result.FeedType = "atom" 463 return result, nil 464} 465 466func (t *DefaultAtomTranslator) translateFeedItem(entry *atom.Entry) (item *Item) { 467 item = &Item{} 468 item.Title = t.translateItemTitle(entry) 469 item.Description = t.translateItemDescription(entry) 470 item.Content = t.translateItemContent(entry) 471 item.Link = t.translateItemLink(entry) 472 item.Updated = t.translateItemUpdated(entry) 473 item.UpdatedParsed = t.translateItemUpdatedParsed(entry) 474 item.Published = t.translateItemPublished(entry) 475 item.PublishedParsed = t.translateItemPublishedParsed(entry) 476 item.Author = t.translateItemAuthor(entry) 477 item.GUID = t.translateItemGUID(entry) 478 item.Image = t.translateItemImage(entry) 479 item.Categories = t.translateItemCategories(entry) 480 item.Enclosures = t.translateItemEnclosures(entry) 481 item.Extensions = entry.Extensions 482 return 483} 484 485func (t *DefaultAtomTranslator) translateFeedTitle(atom *atom.Feed) (title string) { 486 return atom.Title 487} 488 489func (t *DefaultAtomTranslator) translateFeedDescription(atom *atom.Feed) (desc string) { 490 return atom.Subtitle 491} 492 493func (t *DefaultAtomTranslator) translateFeedLink(atom *atom.Feed) (link string) { 494 l := t.firstLinkWithType("alternate", atom.Links) 495 if l != nil { 496 link = l.Href 497 } 498 return 499} 500 501func (t *DefaultAtomTranslator) translateFeedFeedLink(atom *atom.Feed) (link string) { 502 feedLink := t.firstLinkWithType("self", atom.Links) 503 if feedLink != nil { 504 link = feedLink.Href 505 } 506 return 507} 508 509func (t *DefaultAtomTranslator) translateFeedUpdated(atom *atom.Feed) (updated string) { 510 return atom.Updated 511} 512 513func (t *DefaultAtomTranslator) translateFeedUpdatedParsed(atom *atom.Feed) (updated *time.Time) { 514 return atom.UpdatedParsed 515} 516 517func (t *DefaultAtomTranslator) translateFeedAuthor(atom *atom.Feed) (author *Person) { 518 a := t.firstPerson(atom.Authors) 519 if a != nil { 520 feedAuthor := Person{} 521 feedAuthor.Name = a.Name 522 feedAuthor.Email = a.Email 523 author = &feedAuthor 524 } 525 return 526} 527 528func (t *DefaultAtomTranslator) translateFeedLanguage(atom *atom.Feed) (language string) { 529 return atom.Language 530} 531 532func (t *DefaultAtomTranslator) translateFeedImage(atom *atom.Feed) (image *Image) { 533 if atom.Logo != "" { 534 feedImage := Image{} 535 feedImage.URL = atom.Logo 536 image = &feedImage 537 } 538 return 539} 540 541func (t *DefaultAtomTranslator) translateFeedCopyright(atom *atom.Feed) (rights string) { 542 return atom.Rights 543} 544 545func (t *DefaultAtomTranslator) translateFeedGenerator(atom *atom.Feed) (generator string) { 546 if atom.Generator != nil { 547 if atom.Generator.Value != "" { 548 generator += atom.Generator.Value 549 } 550 if atom.Generator.Version != "" { 551 generator += " v" + atom.Generator.Version 552 } 553 if atom.Generator.URI != "" { 554 generator += " " + atom.Generator.URI 555 } 556 generator = strings.TrimSpace(generator) 557 } 558 return 559} 560 561func (t *DefaultAtomTranslator) translateFeedCategories(atom *atom.Feed) (categories []string) { 562 if atom.Categories != nil { 563 categories = []string{} 564 for _, c := range atom.Categories { 565 categories = append(categories, c.Term) 566 } 567 } 568 return 569} 570 571func (t *DefaultAtomTranslator) translateFeedItems(atom *atom.Feed) (items []*Item) { 572 items = []*Item{} 573 for _, entry := range atom.Entries { 574 items = append(items, t.translateFeedItem(entry)) 575 } 576 return 577} 578 579func (t *DefaultAtomTranslator) translateItemTitle(entry *atom.Entry) (title string) { 580 return entry.Title 581} 582 583func (t *DefaultAtomTranslator) translateItemDescription(entry *atom.Entry) (desc string) { 584 return entry.Summary 585} 586 587func (t *DefaultAtomTranslator) translateItemContent(entry *atom.Entry) (content string) { 588 if entry.Content != nil { 589 content = entry.Content.Value 590 } 591 return 592} 593 594func (t *DefaultAtomTranslator) translateItemLink(entry *atom.Entry) (link string) { 595 l := t.firstLinkWithType("alternate", entry.Links) 596 if l != nil { 597 link = l.Href 598 } 599 return 600} 601 602func (t *DefaultAtomTranslator) translateItemUpdated(entry *atom.Entry) (updated string) { 603 return entry.Updated 604} 605 606func (t *DefaultAtomTranslator) translateItemUpdatedParsed(entry *atom.Entry) (updated *time.Time) { 607 return entry.UpdatedParsed 608} 609 610func (t *DefaultAtomTranslator) translateItemPublished(entry *atom.Entry) (updated string) { 611 return entry.Published 612} 613 614func (t *DefaultAtomTranslator) translateItemPublishedParsed(entry *atom.Entry) (updated *time.Time) { 615 return entry.PublishedParsed 616} 617 618func (t *DefaultAtomTranslator) translateItemAuthor(entry *atom.Entry) (author *Person) { 619 a := t.firstPerson(entry.Authors) 620 if a != nil { 621 author = &Person{} 622 author.Name = a.Name 623 author.Email = a.Email 624 } 625 return 626} 627 628func (t *DefaultAtomTranslator) translateItemGUID(entry *atom.Entry) (guid string) { 629 return entry.ID 630} 631 632func (t *DefaultAtomTranslator) translateItemImage(entry *atom.Entry) (image *Image) { 633 return nil 634} 635 636func (t *DefaultAtomTranslator) translateItemCategories(entry *atom.Entry) (categories []string) { 637 if entry.Categories != nil { 638 categories = []string{} 639 for _, c := range entry.Categories { 640 categories = append(categories, c.Term) 641 } 642 } 643 return 644} 645 646func (t *DefaultAtomTranslator) translateItemEnclosures(entry *atom.Entry) (enclosures []*Enclosure) { 647 if entry.Links != nil { 648 enclosures = []*Enclosure{} 649 for _, e := range entry.Links { 650 if e.Rel == "enclosure" { 651 enclosure := &Enclosure{} 652 enclosure.URL = e.Href 653 enclosure.Length = e.Length 654 enclosure.Type = e.Type 655 enclosures = append(enclosures, enclosure) 656 } 657 } 658 659 if len(enclosures) == 0 { 660 enclosures = nil 661 } 662 } 663 return 664} 665 666func (t *DefaultAtomTranslator) firstLinkWithType(linkType string, links []*atom.Link) *atom.Link { 667 if links == nil { 668 return nil 669 } 670 671 for _, link := range links { 672 if link.Rel == linkType { 673 return link 674 } 675 } 676 return nil 677} 678 679func (t *DefaultAtomTranslator) firstPerson(persons []*atom.Person) (person *atom.Person) { 680 if persons == nil || len(persons) == 0 { 681 return 682 } 683 684 person = persons[0] 685 return 686} 687