diff options
Diffstat (limited to 'src/importers')
-rw-r--r-- | src/importers/Makefile.am | 15 | ||||
-rw-r--r-- | src/importers/baseimporter.cpp | 399 | ||||
-rw-r--r-- | src/importers/baseimporter.h | 126 | ||||
-rw-r--r-- | src/importers/kredbimporter.cpp | 67 | ||||
-rw-r--r-- | src/importers/kredbimporter.h | 38 | ||||
-rw-r--r-- | src/importers/kreimporter.cpp | 309 | ||||
-rw-r--r-- | src/importers/kreimporter.h | 54 | ||||
-rw-r--r-- | src/importers/mmfimporter.cpp | 336 | ||||
-rw-r--r-- | src/importers/mmfimporter.h | 65 | ||||
-rw-r--r-- | src/importers/mx2importer.cpp | 186 | ||||
-rw-r--r-- | src/importers/mx2importer.h | 46 | ||||
-rw-r--r-- | src/importers/mxpimporter.cpp | 382 | ||||
-rw-r--r-- | src/importers/mxpimporter.h | 45 | ||||
-rw-r--r-- | src/importers/nycgenericimporter.cpp | 196 | ||||
-rw-r--r-- | src/importers/nycgenericimporter.h | 44 | ||||
-rw-r--r-- | src/importers/recipemlimporter.cpp | 376 | ||||
-rw-r--r-- | src/importers/recipemlimporter.h | 53 | ||||
-rw-r--r-- | src/importers/rezkonvimporter.cpp | 291 | ||||
-rw-r--r-- | src/importers/rezkonvimporter.h | 42 |
19 files changed, 3070 insertions, 0 deletions
diff --git a/src/importers/Makefile.am b/src/importers/Makefile.am new file mode 100644 index 0000000..28ab637 --- /dev/null +++ b/src/importers/Makefile.am @@ -0,0 +1,15 @@ +## Makefile.am for krecipes + +# this is the program that gets installed. it's name is used for all +# of the other Makefile.am variables + +# set the include path for X, tqt and TDE +INCLUDES = -I$(srcdir)/.. -I$(srcdir)/../backends $(all_includes) + +noinst_LTLIBRARIES=libkrecipesimporters.la +libkrecipesimporters_la_SOURCES = mx2importer.cpp mmfimporter.cpp mxpimporter.cpp nycgenericimporter.cpp recipemlimporter.cpp baseimporter.cpp kreimporter.cpp rezkonvimporter.cpp kredbimporter.cpp +libkrecipesimporters_la_METASOURCES=AUTO + +#the library search path. +libkrecipesimporters_la_LDFLAGS = $(KDE_RPATH) $(all_libraries) +noinst_HEADERS = kreimporter.h diff --git a/src/importers/baseimporter.cpp b/src/importers/baseimporter.cpp new file mode 100644 index 0000000..b0ecd61 --- /dev/null +++ b/src/importers/baseimporter.cpp @@ -0,0 +1,399 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "baseimporter.h" + +#include <tdeapplication.h> +#include <tdeconfig.h> +#include <kdebug.h> +#include <tdelocale.h> +#include <kprogress.h> +#include <tdemessagebox.h> + +#include <tqvaluevector.h> + +#include "datablocks/recipe.h" +#include "backends/recipedb.h" +#include "datablocks/categorytree.h" +#include "datablocks/unit.h" + +BaseImporter::BaseImporter() : + m_recipe_list( new RecipeList ), + m_cat_structure( 0 ), + file_recipe_count( 0 ) +{ + TDEConfig * config = kapp->config(); + config->setGroup( "Import" ); + + direct = config->readBoolEntry( "DirectImport", false ); +} + +BaseImporter::~BaseImporter() +{ + delete m_recipe_list; + delete m_cat_structure; +} + +void BaseImporter::add( const RecipeList &recipe_list ) +{ + file_recipe_count += recipe_list.count(); + + for ( RecipeList::const_iterator it = recipe_list.begin(); it != recipe_list.end(); ++it ) { + Recipe copy = *it; + copy.recipeID = -1; //make sure an importer didn't give this a value + for ( RatingList::iterator rating_it = copy.ratingList.begin(); rating_it != copy.ratingList.end(); ++rating_it ) { + (*rating_it).id = -1; + } + m_recipe_list->append( copy ); + } + + if ( direct ) { + if ( !m_progress_dialog->wasCancelled() ) + importRecipes( *m_recipe_list, m_database, m_progress_dialog ); + } +} + +void BaseImporter::add( const Recipe &recipe ) +{ + file_recipe_count++; + Recipe copy = recipe; + copy.recipeID = -1; //make sure an importer didn't give this a value + + if ( direct ) { + if ( !m_progress_dialog->wasCancelled() ) { + RecipeList list; + list.append( recipe ); + importRecipes( list, m_database, m_progress_dialog ); + } + } + else + m_recipe_list->append( copy ); +} + +void BaseImporter::parseFiles( const TQStringList &filenames ) +{ + if ( direct ) + m_filenames = filenames; + else { + for ( TQStringList::const_iterator file_it = filenames.begin(); file_it != filenames.end(); ++file_it ) { + file_recipe_count = 0; + parseFile( *file_it ); + processMessages( *file_it ); + } + } +} + +void BaseImporter::import( RecipeDB *db, bool automatic ) +{ + if ( direct ) { + m_database = db; + + m_progress_dialog = new KProgressDialog( kapp->mainWidget(), 0, + i18n( "Importing selected recipes" ), TQString::null, true ); + KProgress *progress = m_progress_dialog->progressBar(); + progress->setPercentageVisible(false); + progress->setTotalSteps( 0 ); + + for ( TQStringList::const_iterator file_it = m_filenames.begin(); file_it != m_filenames.end(); ++file_it ) { + file_recipe_count = 0; + parseFile( *file_it ); + processMessages( *file_it ); + + if ( m_progress_dialog->wasCancelled() ) + break; + } + + importUnitRatios( db ); + delete m_progress_dialog; + } + else { + if ( m_recipe_list->count() == 0 ) + return; + + m_recipe_list->empty(); + //db->blockSignals(true); + + m_progress_dialog = new KProgressDialog( kapp->mainWidget(), 0, + i18n( "Importing selected recipes" ), TQString::null, true ); + KProgress *progress = m_progress_dialog->progressBar(); + progress->setTotalSteps( m_recipe_list->count() ); + progress->setFormat( i18n( "%v/%m Recipes" ) ); + + if ( m_cat_structure ) { + importCategoryStructure( db, m_cat_structure ); + delete m_cat_structure; + m_cat_structure = 0; + } + importRecipes( *m_recipe_list, db, m_progress_dialog ); + importUnitRatios( db ); + + //db->blockSignals(false); + delete m_progress_dialog; m_progress_dialog = 0; + } +} + +void BaseImporter::importIngredient( IngredientData &ing, RecipeDB *db, KProgressDialog *progress_dialog ) +{ + //cache some data we'll need + int max_units_length = db->maxUnitNameLength(); + int max_group_length = db->maxIngGroupNameLength(); + + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + //create ingredient groups + TQString real_group_name = ing.group.left( max_group_length ); + int new_group_id = db->findExistingIngredientGroupByName(real_group_name); + if ( new_group_id == -1 ) { + db->createNewIngGroup( real_group_name ); + new_group_id = db->lastInsertID(); + } + ing.groupID = new_group_id; + + int new_ing_id = db->findExistingIngredientByName(ing.name); + if ( new_ing_id == -1 && !ing.name.isEmpty() ) + { + db->createNewIngredient( ing.name ); + new_ing_id = db->lastInsertID(); + } + + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + Unit real_unit( ing.units.name.left( max_units_length ), ing.units.plural.left( max_units_length ) ); + if ( real_unit.name.isEmpty() ) + real_unit.name = real_unit.plural; + else if ( real_unit.plural.isEmpty() ) + real_unit.plural = real_unit.name; + + int new_unit_id = db->findExistingUnitByName(real_unit.name); + if ( new_unit_id == -1 ) { + db->createNewUnit( Unit(real_unit.name, real_unit.plural) ); + new_unit_id = db->lastInsertID(); + } + + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + if ( ing.prepMethodList.count() > 0 ) { + for ( ElementList::iterator prep_it = ing.prepMethodList.begin(); prep_it != ing.prepMethodList.end(); ++prep_it ) { + int new_prep_id = db->findExistingPrepByName((*prep_it).name); + if ( new_prep_id == -1 ) { + db->createNewPrepMethod((*prep_it).name); + new_prep_id = db->lastInsertID(); + } + (*prep_it).id = new_prep_id; + } + } + + ing.units.id = new_unit_id; + ing.ingredientID = new_ing_id; + + if ( !db->ingredientContainsUnit( new_ing_id, new_unit_id ) ) + db->addUnitToIngredient( new_ing_id, new_unit_id ); +} + +void BaseImporter::importRecipes( RecipeList &selected_recipes, RecipeDB *db, KProgressDialog *progress_dialog ) +{ + // Load Current Settings + TDEConfig *config = kapp->config(); + config->setGroup( "Import" ); + bool overwrite = config->readBoolEntry( "OverwriteExisting", false ); + + RecipeList::iterator recipe_it; RecipeList::iterator recipe_list_end( selected_recipes.end() ); + RecipeList::iterator recipe_it_old = selected_recipes.end(); + for ( recipe_it = selected_recipes.begin(); recipe_it != recipe_list_end; ++recipe_it ) { + if ( !direct ) { + if ( progress_dialog->wasCancelled() ) { + KMessageBox::information( kapp->mainWidget(), i18n( "All recipes up unto this point have been successfully imported." ) ); + //db->blockSignals(false); + return ; + } + } + + if ( recipe_it_old != selected_recipes.end() ) + selected_recipes.remove( recipe_it_old ); + + progress_dialog->setLabel( TQString( i18n( "Importing recipe: %1" ) ).arg( ( *recipe_it ).title ) ); + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + + //add all recipe items (authors, ingredients, etc. to the database if they aren't already + IngredientList::iterator ing_list_end( ( *recipe_it ).ingList.end() ); + for ( IngredientList::iterator ing_it = ( *recipe_it ).ingList.begin(); ing_it != ing_list_end; ++ing_it ) { + importIngredient( *ing_it, db, progress_dialog ); + + for ( TQValueList<IngredientData>::iterator sub_it = (*ing_it).substitutes.begin(); sub_it != (*ing_it).substitutes.end(); ++sub_it ) { + importIngredient( *sub_it, db, progress_dialog ); + } + } + + ElementList::iterator author_list_end( ( *recipe_it ).authorList.end() ); + for ( ElementList::iterator author_it = ( *recipe_it ).authorList.begin(); author_it != author_list_end; ++author_it ) { + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + int new_author_id = db->findExistingAuthorByName(( *author_it ).name); + if ( new_author_id == -1 && !( *author_it ).name.isEmpty() ) { + db->createNewAuthor( ( *author_it ).name ); + new_author_id = db->lastInsertID(); + } + + ( *author_it ).id = new_author_id; + } + + ElementList::iterator cat_list_end( ( *recipe_it ).categoryList.end() ); + for ( ElementList::iterator cat_it = ( *recipe_it ).categoryList.begin(); cat_it != cat_list_end; ++cat_it ) { + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + int new_cat_id = db->findExistingCategoryByName(( *cat_it ).name); + if ( new_cat_id == -1 && !( *cat_it ).name.isEmpty() ) { + db->createNewCategory( ( *cat_it ).name ); + new_cat_id = db->lastInsertID(); + } + + ( *cat_it ).id = new_cat_id; + } + + if ( !(*recipe_it).yield.type.isEmpty() ) { + int new_id = db->findExistingYieldTypeByName((*recipe_it).yield.type); + if ( new_id == -1 ) { + db->createNewYieldType( (*recipe_it).yield.type ); + new_id = db->lastInsertID(); + } + (*recipe_it).yield.type_id = new_id; + } + + RatingList::iterator rating_list_end( ( *recipe_it ).ratingList.end() ); + for ( RatingList::iterator rating_it = ( *recipe_it ).ratingList.begin(); rating_it != rating_list_end; ++rating_it ) { + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + for ( RatingCriteriaList::iterator rc_it = (*rating_it).ratingCriteriaList.begin(); rc_it != (*rating_it).ratingCriteriaList.end(); ++rc_it ) { + int new_criteria_id = db->findExistingRatingByName(( *rc_it ).name); + if ( new_criteria_id == -1 && !( *rc_it ).name.isEmpty() ) { + db->createNewRating( ( *rc_it ).name ); + new_criteria_id = db->lastInsertID(); + } + + ( *rc_it ).id = new_criteria_id; + } + } + + if ( overwrite ) //overwrite existing + ( *recipe_it ).recipeID = db->findExistingRecipeByName( ( *recipe_it ).title ); + else //rename + ( *recipe_it ).title = db->getUniqueRecipeTitle( ( *recipe_it ).title ); + + if ( direct ) { + progress_dialog->progressBar()->advance( 1 ); + kapp->processEvents(); + } + + //save into the database + db->saveRecipe( &( *recipe_it ) ); + + recipe_it_old = recipe_it; //store to delete once we've got the next recipe + } +} + +void BaseImporter::setCategoryStructure( CategoryTree *cat_structure ) +{ + if ( direct ) { + importCategoryStructure( m_database, cat_structure ); + } + else { + delete m_cat_structure; + m_cat_structure = cat_structure; + } +} + +void BaseImporter::importCategoryStructure( RecipeDB *db, const CategoryTree *categoryTree ) +{ + for ( CategoryTree * child_it = categoryTree->firstChild(); child_it; child_it = child_it->nextSibling() ) { + int new_cat_id = db->findExistingCategoryByName( child_it->category.name ); + if ( new_cat_id == -1 ) { + db->createNewCategory( child_it->category.name, categoryTree->category.id ); + new_cat_id = db->lastInsertID(); + } + + child_it->category.id = new_cat_id; + + importCategoryStructure( db, child_it ); + } +} + +void BaseImporter::setUnitRatioInfo( UnitRatioList &ratioList, UnitList &unitList ) +{ + m_ratioList = ratioList; + m_unitList = unitList; +} + +void BaseImporter::importUnitRatios( RecipeDB *db ) +{ + for ( UnitRatioList::const_iterator it = m_ratioList.begin(); it != m_ratioList.end(); ++it ) { + TQString unitName1, unitName2; + for ( UnitList::const_iterator unit_it = m_unitList.begin(); unit_it != m_unitList.end(); ++unit_it ) { + if ( ( *it ).uID1 == ( *unit_it ).id ) { + unitName1 = ( *unit_it ).name; + if ( !unitName2.isNull() ) + break; + } + else if ( ( *it ).uID2 == ( *unit_it ).id ) { + unitName2 = ( *unit_it ).name; + if ( !unitName1.isNull() ) + break; + } + } + + int unitId1 = db->findExistingUnitByName( unitName1 ); + int unitId2 = db->findExistingUnitByName( unitName2 ); + + //the unit needed for the ratio may not have been added, because the + //recipes chosen did not include the unit + if ( unitId1 != -1 && unitId2 != -1 ) { + UnitRatio ratio; + ratio.uID1 = unitId1; + ratio.uID2 = unitId2; + ratio.ratio = ( *it ).ratio; + db->saveUnitRatio( &ratio ); + } + } +} + +void BaseImporter::processMessages( const TQString &file ) +{ + if ( m_error_msgs.count() > 0 ) { + //<!doc> ensures it is detected as RichText + m_master_error += TQString( i18n( "<!doc>Import of recipes from the file <b>\"%1\"</b> <b>failed</b> due to the following error(s):" ) ).arg( file ); + m_master_error += "<ul><li>" + m_error_msgs.join( "</li><li>" ) + "</li></ul>"; + + m_error_msgs.clear(); + } + else if ( m_warning_msgs.count() > 0 ) { + m_master_warning += TQString( i18n( "The file <b>%1</b> generated the following warning(s):" ) ).arg( file ); + m_master_warning += "<ul><li>" + m_warning_msgs.join( "</li><li>" ) + "</li></ul>"; + + m_warning_msgs.clear(); + } +} diff --git a/src/importers/baseimporter.h b/src/importers/baseimporter.h new file mode 100644 index 0000000..944f50e --- /dev/null +++ b/src/importers/baseimporter.h @@ -0,0 +1,126 @@ +/*************************************************************************** +* Copyright (C) 2003-2005 by * +* Unai Garro <[email protected]> * +* Cyril Bosselut ([email protected]) * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef BASEIMPORTER_H +#define BASEIMPORTER_H + +#include <tdelocale.h> + +#include <tqstring.h> +#include <tqstringlist.h> + +#include "datablocks/recipelist.h" +#include "datablocks/elementlist.h" +#include "datablocks/unitratiolist.h" + +class Recipe; +class RecipeDB; +class CategoryTree; +class IngredientData; + +class KProgressDialog; + +/** @brief Subclass this class to create an importer for a specific file type. + * + * Subclasses should take the file name of the file to import in their constructor + * and then parse the file. For every recipe found in the file, a Recipe object should + * be created and added to the importer using the @ref add() function. + * + * @author Jason Kivlighn + */ +class BaseImporter +{ +public: + BaseImporter(); + virtual ~BaseImporter(); + + TQString getMessages() const + { + return m_master_error + m_master_warning; + } + TQString getErrorMsg() const + { + return m_master_error; + } + TQString getWarningMsg() const + { + return m_master_warning; + } + + void parseFiles( const TQStringList &filenames ); + + /** Import all the recipes into the given database. These recipes are the + * recipes added to this class by a subclass using the @ref add() method. + */ + void import( RecipeDB *db, bool automatic = false ); + + RecipeList recipeList() const { return *m_recipe_list; } + void setRecipeList( const RecipeList &list ) { *m_recipe_list = list; } + + const CategoryTree *categoryStructure() const { return m_cat_structure; } + +protected: + virtual void parseFile( const TQString &filename ) = 0; + + void importRecipes( RecipeList &selected_recipes, RecipeDB *db, KProgressDialog *progess_dialog ); + + /** Add a recipe to be imported into the database */ + void add( const Recipe &recipe ); + void add( const RecipeList &recipe_list ); + + void setCategoryStructure( CategoryTree *cat_structure ); + void setUnitRatioInfo( UnitRatioList &ratioList, UnitList &unitList ); + + int totalCount() const + { + return m_recipe_list->count(); + } + int fileRecipeCount() const + { + return file_recipe_count; + } + + void setErrorMsg( const TQString & s ) + { + m_error_msgs.append( s ); + } + void addWarningMsg( const TQString & s ) + { + m_warning_msgs.append( s ); + } + +private: + void importCategoryStructure( RecipeDB *, const CategoryTree * ); + void importUnitRatios( RecipeDB * ); + void importIngredient( IngredientData &ing, RecipeDB *db, KProgressDialog *progress_dialog ); + + void processMessages( const TQString &file ); + + RecipeList *m_recipe_list; + CategoryTree *m_cat_structure; + UnitRatioList m_ratioList; + UnitList m_unitList; + + TQStringList m_warning_msgs; + TQStringList m_error_msgs; + TQString m_master_warning; + TQString m_master_error; + + int file_recipe_count; + bool direct; + + RecipeDB *m_database; + KProgressDialog *m_progress_dialog; + TQStringList m_filenames; +}; + +#endif //BASEIMPORTER_H diff --git a/src/importers/kredbimporter.cpp b/src/importers/kredbimporter.cpp new file mode 100644 index 0000000..ebb4d3c --- /dev/null +++ b/src/importers/kredbimporter.cpp @@ -0,0 +1,67 @@ +/*************************************************************************** +* Copyright (C) 2004 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "kredbimporter.h" + +#include <tdeapplication.h> +#include <tdeconfig.h> +#include <kdebug.h> + +#include "datablocks/recipelist.h" +#include "datablocks/categorytree.h" +#include "backends/recipedb.h" + +KreDBImporter::KreDBImporter( const TQString &_dbType, const TQString &_host, const TQString &_user, const TQString &_pass, int _port ) : BaseImporter(), + dbType( _dbType ), + host( _host ), + user( _user ), + pass( _pass ), + port( _port ) +{} + +KreDBImporter::~KreDBImporter() +{} + +void KreDBImporter::parseFile( const TQString &file ) //this is either a database file or a database table +{ + RecipeDB * database = RecipeDB::createDatabase( dbType, host, user, pass, file, port, file ); //uses 'file' as either table or file name, depending on the database + + if ( database ) { + database->connect( false ); //don't create the database if it fails to connect + + if ( database->ok() ) { + //set the category structure + CategoryTree * tree = new CategoryTree; + database->loadCategories( tree ); + setCategoryStructure( tree ); + + #if 0 + //set unit ratios + UnitRatioList ratioList; + UnitList unitList; + database->loadUnitRatios( &ratioList ); + database->loadUnits( &unitList ); + + setUnitRatioInfo( ratioList, unitList ); + #endif + + //now load recipes + RecipeList recipes; + database->loadRecipes( &recipes, RecipeDB::All ^ RecipeDB::Properties ); + + //now add these recipes to the importer + add( recipes ); + } + else + setErrorMsg( database->err() ); + } + + delete database; +} diff --git a/src/importers/kredbimporter.h b/src/importers/kredbimporter.h new file mode 100644 index 0000000..1afc8e8 --- /dev/null +++ b/src/importers/kredbimporter.h @@ -0,0 +1,38 @@ +/*************************************************************************** +* Copyright (C) 2004 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef KREDBIMPORTER_H +#define KREDBIMPORTER_H + +#include <tqstring.h> + +#include "baseimporter.h" + +/** Class to import recipes from any other Krecipes database backend. + * Note: Though independant of database type, the two databases must have the same structure (i.e. be the same version) + * @author Jason Kivlighn + */ +class KreDBImporter : public BaseImporter +{ +public: + KreDBImporter( const TQString &dbType, const TQString &host = TQString::null, const TQString &user = TQString::null, const TQString &pass = TQString::null, int port = 0 ); + virtual ~KreDBImporter(); + +private: + virtual void parseFile( const TQString &file_or_table ); + + TQString dbType; + TQString host; + TQString user; + TQString pass; + int port; +}; + +#endif //KREDBIMPORTER_H diff --git a/src/importers/kreimporter.cpp b/src/importers/kreimporter.cpp new file mode 100644 index 0000000..2e55589 --- /dev/null +++ b/src/importers/kreimporter.cpp @@ -0,0 +1,309 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Cyril Bosselut ([email protected]) * +* * +* Copyright (C) 2003-2005 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "kreimporter.h" + +#include <tdelocale.h> +#include <kdebug.h> + +#include <tqfile.h> +#include <tqstringlist.h> +#include <kstandarddirs.h> + +#include "datablocks/recipe.h" +#include "datablocks/categorytree.h" + +KreImporter::KreImporter() : BaseImporter() +{} + +void KreImporter::parseFile( const TQString &filename ) +{ + TQFile * file = 0; + bool unlink = false; + kdDebug() << "loading file: %s" << filename << endl; + + if ( filename.right( 4 ) == ".kre" ) { + file = new TQFile( filename ); + kdDebug() << "file is an archive" << endl; + KTar* kre = new KTar( filename, "application/x-gzip" ); + kre->open( IO_ReadOnly ); + const KArchiveDirectory* dir = kre->directory(); + TQString name; + TQStringList fileList = dir->entries(); + for ( TQStringList::Iterator it = fileList.begin(); it != fileList.end(); ++it ) { + if ( ( *it ).right( 6 ) == ".kreml" ) { + name = *it; + } + } + if ( name.isEmpty() ) { + kdDebug() << "error: Archive doesn't contain a valid Krecipes file" << endl; + setErrorMsg( i18n( "Archive does not contain a valid Krecipes file" ) ); + return ; + } + TQString tmp_dir = locateLocal( "tmp", "" ); + dir->copyTo( tmp_dir ); + file = new TQFile( tmp_dir + name ); + kre->close(); + unlink = true; //remove file after import + } + else { + file = new TQFile( filename ); + } + + if ( file->open( IO_ReadOnly ) ) { + kdDebug() << "file opened" << endl; + TQDomDocument doc; + TQString error; + int line; + int column; + if ( !doc.setContent( file, &error, &line, &column ) ) { + kdDebug() << TQString( "error: \"%1\" at line %2, column %3" ).arg( error ).arg( line ).arg( column ) << endl; + setErrorMsg( TQString( i18n( "\"%1\" at line %2, column %3" ) ).arg( error ).arg( line ).arg( column ) ); + return ; + } + + TQDomElement kreml = doc.documentElement(); + + if ( kreml.tagName() != "krecipes" ) { + setErrorMsg( i18n( "This file does not appear to be a *.kreml file" ) ); + return ; + } + + // TODO Check if there are changes between versions + TQString kreVersion = kreml.attribute( "version" ); + kdDebug() << TQString( i18n( "KreML version %1" ) ).arg( kreVersion ) << endl; + + TQDomNodeList r = kreml.childNodes(); + TQDomElement krecipe; + + for ( unsigned z = 0; z < r.count(); z++ ) { + krecipe = r.item( z ).toElement(); + TQDomNodeList l = krecipe.childNodes(); + if ( krecipe.tagName() == "krecipes-recipe" ) { + Recipe recipe; + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + if ( el.tagName() == "krecipes-description" ) { + readDescription( el.childNodes(), &recipe ); + } + if ( el.tagName() == "krecipes-ingredients" ) { + readIngredients( el.childNodes(), &recipe ); + } + if ( el.tagName() == "krecipes-instructions" ) { + recipe.instructions = el.text().stripWhiteSpace(); + } + if ( el.tagName() == "krecipes-ratings" ) { + readRatings( el.childNodes(), &recipe ); + } + } + add + ( recipe ); + } + else if ( krecipe.tagName() == "krecipes-category-structure" ) { + CategoryTree * tree = new CategoryTree; + readCategoryStructure( l, tree ); + setCategoryStructure( tree ); + } + } + } + if ( unlink ) { + file->remove + (); + } +} + +KreImporter::~KreImporter() +{ +} + +void KreImporter::readCategoryStructure( const TQDomNodeList& l, CategoryTree *tree ) +{ + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + + TQString category = el.attribute( "name" ); + CategoryTree *child_node = tree->add + ( Element( category ) ); + readCategoryStructure( el.childNodes(), child_node ); + } +} + +void KreImporter::readDescription( const TQDomNodeList& l, Recipe *recipe ) +{ + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + if ( el.tagName() == "title" ) { + recipe->title = el.text(); + kdDebug() << "Found title: " << recipe->title << endl; + } + else if ( el.tagName() == "author" ) { + kdDebug() << "Found author: " << el.text() << endl; + recipe->authorList.append( Element( el.text() ) ); + } + else if ( el.tagName() == "serving" ) { //### Keep for < 0.9 compatibility + recipe->yield.amount = el.text().toInt(); + } + else if ( el.tagName() == "yield" ) { + TQDomNodeList yield_children = el.childNodes(); + for ( unsigned j = 0; j < yield_children.count(); j++ ) { + TQDomElement y = yield_children.item( j ).toElement(); + if ( y.tagName() == "amount" ) + readAmount(y,recipe->yield.amount,recipe->yield.amount_offset); + else if ( y.tagName() == "type" ) + recipe->yield.type = y.text(); + } + } + else if ( el.tagName() == "preparation-time" ) { + recipe->prepTime = TQTime::fromString( el.text() ); + } + else if ( el.tagName() == "category" ) { + TQDomNodeList categories = el.childNodes(); + for ( unsigned j = 0; j < categories.count(); j++ ) { + TQDomElement c = categories.item( j ).toElement(); + if ( c.tagName() == "cat" ) { + kdDebug() << "Found category: " << TQString( c.text() ).stripWhiteSpace() << endl; + recipe->categoryList.append( Element( TQString( c.text() ).stripWhiteSpace() ) ); + } + } + } + else if ( el.tagName() == "pictures" ) { + if ( el.hasChildNodes() ) { + TQDomNodeList pictures = el.childNodes(); + for ( unsigned j = 0; j < pictures.count(); j++ ) { + TQDomElement pic = pictures.item( j ).toElement(); + TQCString decodedPic; + if ( pic.tagName() == "pic" ) + kdDebug() << "Found photo" << endl; + TQPixmap pix; + KCodecs::base64Decode( TQCString( pic.text().latin1() ), decodedPic ); + int len = decodedPic.size(); + TQByteArray picData( len ); + memcpy( picData.data(), decodedPic.data(), len ); + bool ok = pix.loadFromData( picData, "JPEG" ); + if ( ok ) { + recipe->photo = pix; + } + } + } + } + } +} + +void KreImporter::readIngredients( const TQDomNodeList& l, Recipe *recipe, const TQString &header, Ingredient *ing ) +{ + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + if ( el.tagName() == "ingredient" ) { + TQDomNodeList ingredient = el.childNodes(); + Ingredient new_ing; + for ( unsigned j = 0; j < ingredient.count(); j++ ) { + TQDomElement ing = ingredient.item( j ).toElement(); + if ( ing.tagName() == "name" ) { + new_ing.name = TQString( ing.text() ).stripWhiteSpace(); + kdDebug() << "Found ingredient: " << new_ing.name << endl; + } + else if ( ing.tagName() == "amount" ) { + readAmount(ing,new_ing.amount,new_ing.amount_offset); + } + else if ( ing.tagName() == "unit" ) { + new_ing.units = Unit( ing.text().stripWhiteSpace(), new_ing.amount+new_ing.amount_offset ); + } + else if ( ing.tagName() == "prep" ) { + new_ing.prepMethodList = ElementList::split(",",TQString( ing.text() ).stripWhiteSpace()); + } + else if ( ing.tagName() == "substitutes" ) { + readIngredients(ing.childNodes(), recipe, header, &new_ing); + } + } + new_ing.group = header; + + if ( !ing ) + recipe->ingList.append( new_ing ); + else + ing->substitutes.append( new_ing ); + } + else if ( el.tagName() == "ingredient-group" ) { + readIngredients( el.childNodes(), recipe, el.attribute( "name" ) ); + } + } +} + +void KreImporter::readAmount( const TQDomElement& amountEl, double &amount, double &amount_offset ) +{ + TQDomNodeList children = amountEl.childNodes(); + + double min = 0,max = 0; + for ( unsigned i = 0; i < children.count(); i++ ) { + TQDomElement child = children.item( i ).toElement(); + if ( child.tagName() == "min" ) { + min = ( TQString( child.text() ).stripWhiteSpace() ).toDouble(); + } + else if ( child.tagName() == "max" ) + max = ( TQString( child.text() ).stripWhiteSpace() ).toDouble(); + else if ( child.tagName().isEmpty() ) + min = ( TQString( amountEl.text() ).stripWhiteSpace() ).toDouble(); + } + + amount = min; + if ( max > 0 ) + amount_offset = max-min; +} + +void KreImporter::readRatings( const TQDomNodeList& l, Recipe *recipe ) +{ + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement child = l.item( i ).toElement(); + if ( child.tagName() == "rating" ) { + Rating r; + + TQDomNodeList ratingChildren = child.childNodes(); + for ( unsigned j = 0; j < ratingChildren.count(); j++ ) { + TQDomElement ratingChild = ratingChildren.item( j ).toElement(); + if ( ratingChild.tagName() == "comment" ) { + r.comment = ratingChild.text(); + } + else if ( ratingChild.tagName() == "rater" ) { + r.rater = ratingChild.text(); + } + else if ( ratingChild.tagName() == "criterion" ) { + readCriterion(ratingChild.childNodes(),r.ratingCriteriaList); + } + } + recipe->ratingList.append(r); + } + } +} + +void KreImporter::readCriterion( const TQDomNodeList& l, RatingCriteriaList &rc_list ) +{ + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement child = l.item( i ).toElement(); + + if ( child.tagName() == "criteria" ) { + RatingCriteria rc; + + TQDomNodeList criteriaChildren = child.childNodes(); + for ( unsigned j = 0; j < criteriaChildren.count(); j++ ) { + TQDomElement criteriaChild = criteriaChildren.item( j ).toElement(); + + if ( criteriaChild.tagName() == "name" ) { + rc.name = criteriaChild.text(); + } + else if ( criteriaChild.tagName() == "stars" ) { + rc.stars = criteriaChild.text().toDouble(); + } + } + rc_list.append(rc); + } + } +} diff --git a/src/importers/kreimporter.h b/src/importers/kreimporter.h new file mode 100644 index 0000000..af774d2 --- /dev/null +++ b/src/importers/kreimporter.h @@ -0,0 +1,54 @@ +/*************************************************************************** +* Copyright (C) 2003-2005 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef KREIMPORTER_H +#define KREIMPORTER_H + +#include <tdelocale.h> +#include <kstandarddirs.h> +#include <kmdcodec.h> +#include <ktar.h> +#include <tdetempfile.h> + +#include <tqfile.h> +#include <tqstringlist.h> +#include <tqdom.h> + +#include "baseimporter.h" + +#include "datablocks/recipe.h" + +class Recipe; +class CategoryTree; + +/** +Import for Krecipes native file format (.kre, .kreml) + +@author Cyril Bosselut, Jason Kivlighn +*/ +class KreImporter: public BaseImporter +{ +public: + KreImporter(); + virtual ~KreImporter(); + +private: + void parseFile( const TQString& filename ); + +private: + void readCategoryStructure( const TQDomNodeList& l, CategoryTree *tree ); + void readDescription( const TQDomNodeList& l, Recipe* ); + void readIngredients( const TQDomNodeList& l, Recipe*, const TQString &header = TQString::null, Ingredient *ing = 0 ); + void readAmount( const TQDomElement& amount1, double &amount2, double &amount_offset ); + void readRatings( const TQDomNodeList&, Recipe * ); + void readCriterion( const TQDomNodeList&, RatingCriteriaList &r ); +}; + +#endif diff --git a/src/importers/mmfimporter.cpp b/src/importers/mmfimporter.cpp new file mode 100644 index 0000000..5487d2b --- /dev/null +++ b/src/importers/mmfimporter.cpp @@ -0,0 +1,336 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "mmfimporter.h" + +#include <tdeapplication.h> +#include <tdelocale.h> +#include <kdebug.h> + +#include <tqfile.h> +#include <tqregexp.h> +#include <tqtextstream.h> +#include <tqstringlist.h> + +#include "datablocks/mixednumber.h" +#include "datablocks/recipe.h" +#include "mmdata.h" + +//TODO: pre-parse file and try to correct alignment errors in ingredients? + +MMFImporter::MMFImporter() : BaseImporter() +{} + +MMFImporter::~MMFImporter() +{} + +void MMFImporter::parseFile( const TQString &file ) +{ + resetVars(); + + TQFile input( file ); + + if ( input.open( IO_ReadOnly ) ) { + TQTextStream stream( &input ); + stream.skipWhiteSpace(); + + TQString line; + while ( !stream.atEnd() ) { + line = stream.readLine(); + + if ( line.startsWith( "MMMMM" ) ) { + version = VersionMMMMM; + importMMF( stream ); + } + else if ( line.contains( "Recipe Extracted from Meal-Master (tm) Database" ) ) { + version = FromDatabase; + importMMF( stream ); + } + else if ( line.startsWith( "-----" ) ) { + version = VersionNormal; + importMMF( stream ); + } + else if ( line.startsWith( "MM" ) ) { + version = VersionBB; + ( void ) stream.readLine(); + importMMF( stream ); + } + + stream.skipWhiteSpace(); + } + + if ( fileRecipeCount() == 0 ) + addWarningMsg( i18n( "No recipes found in this file." ) ); + } + else + setErrorMsg( i18n( "Unable to open file." ) ); +} + +void MMFImporter::importMMF( TQTextStream &stream ) +{ + kapp->processEvents(); //don't want the user to think its frozen... especially for files with thousands of recipes + + TQString current; + + //===============FIXED FORMAT================// + //line 1: title + //line 2: categories (comma or space separated) + //line 3: yield (number followed by label) + + //title + stream.skipWhiteSpace(); + current = stream.readLine(); + m_title = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + kdDebug() << "Found title: " << m_title << endl; + + //categories + stream.skipWhiteSpace(); + current = stream.readLine().stripWhiteSpace(); + const char separator = ( version == FromDatabase ) ? ' ' : ','; + TQStringList categories = TQStringList::split( separator, current.mid( current.find( ":" ) + 1, current.length() ) ); + for ( TQStringList::const_iterator it = categories.begin(); it != categories.end(); ++it ) { + Element new_cat; + new_cat.name = TQString( *it ).stripWhiteSpace(); + kdDebug() << "Found category: " << new_cat.name << endl; + m_categories.append( new_cat ); + } + + //servings + stream.skipWhiteSpace(); + current = stream.readLine().stripWhiteSpace(); + if ( current.startsWith( "Yield:" ) ) { + //get the number between the ":" and the next space after it + m_servings = current.mid( current.find( ":" ) + 1, + current.find( " ", current.find( ":" ) + 2 ) - current.find( ":" ) ).toInt(); + kdDebug() << "Found yield: " << m_servings << endl; + } + else if ( current.startsWith( "Servings:" ) ) //from database version + { + m_servings = current.mid( current.find( ":" ) + 1, current.length() ).toInt(); + kdDebug() << "Found servings: " << m_servings << endl; + } + + //=======================VARIABLE FORMAT===================// + //read lines until ending is found + //each line is either an ingredient, ingredient header, or instruction + bool instruction_found = false; + bool is_sub = false; + + ( void ) stream.readLine(); + current = stream.readLine(); + while ( current.stripWhiteSpace() != "MMMMM" && + current.stripWhiteSpace() != "-----" && + current.stripWhiteSpace() != "-----------------------------------------------------------------------------" && + !stream.atEnd() ) { + bool col_one_used = loadIngredientLine( current.left( 41 ), m_left_col_ing, is_sub ); + if ( col_one_used ) //only check for second column if there is an ingredient in the first column + loadIngredientLine( current.mid( 41, current.length() ), m_right_col_ing, is_sub ); + + if ( instruction_found && col_one_used ) { + addWarningMsg( TQString( i18n( "While loading recipe <b>%1</b> " + "an ingredient line was found after the directions. " + "While this is valid, it most commonly indicates an incorrectly " + "formatted recipe." ) ).arg( m_title ) ); + } + + if ( !col_one_used && + !loadIngredientHeader( current.stripWhiteSpace() ) ) { + if ( !current.stripWhiteSpace().isEmpty() ) + instruction_found = true; + m_instructions += current.stripWhiteSpace() + "\n"; + //kdDebug()<<"Found instruction line: "<<current.stripWhiteSpace()<<endl; + } + + current = stream.readLine(); + } + m_instructions = m_instructions.stripWhiteSpace(); + //kdDebug()<<"Found instructions: "<<m_instructions<<endl; + + putDataInRecipe(); +} + +bool MMFImporter::loadIngredientLine( const TQString &string, IngredientList &list, bool &is_sub ) +{ + //just ignore an empty line + if ( string.stripWhiteSpace().isEmpty() ) + return false; + + Ingredient new_ingredient; + new_ingredient.amount = 0; //amount not required, so give default of 0 + + if ( string.at( 11 ) == "-" && string.mid( 0, 11 ).stripWhiteSpace().isEmpty() && !list.isEmpty() ) //continuation of previous ingredient + { + //kdDebug()<<"Appending to last ingredient in column: "<<string.stripWhiteSpace().mid(1,string.length())<<endl; + ( *list.at( list.count() - 1 ) ).name += " " + string.stripWhiteSpace().mid( 1, string.length() ); + TQString name = ( *list.at( list.count() - 1 ) ).name; + + if ( name.endsWith(", or") ) { + ( *list.at( list.count() - 1 ) ).name = name.left(name.length()-4); + is_sub = true; + } + else + is_sub = false; + + return true; + } + + //amount + if ( !string.mid( 0, 7 ).stripWhiteSpace().isEmpty() ) { + bool ok; + MixedNumber amount = MixedNumber::fromString( string.mid( 0, 7 ).stripWhiteSpace(), &ok, false ); + if ( !ok ) + return false; + else + new_ingredient.amount = amount.toDouble(); + } + + //amount/unit separator + if ( string[ 7 ] != ' ' ) + return false; + + //unit + if ( !string.mid( 8, 2 ).stripWhiteSpace().isEmpty() ) { + bool is_unit = false; + TQString unit( string.mid( 8, 2 ).stripWhiteSpace() ); + for ( int i = 0; unit_info[ i ].short_form; i++ ) { + if ( unit_info[ i ].short_form == unit ) { + is_unit = true; + if ( new_ingredient.amount <= 1 ) + unit = unit_info[ i ].expanded_form; + else + unit = unit_info[ i ].plural_expanded_form; + + break; + } + } + if ( !is_unit ) { /*This gives too many false warnings... + addWarningMsg( TQString(i18n("Unit \"%1\" not recognized. " + "Used in the context of \"%2\". If this shouldn't be an ingredient line (i.e. is part of the instructions), " + "then you can safely ignore this warning, and the recipe will be correctly imported.")).arg(unit).arg(string.stripWhiteSpace()) );*/ + return false; + } + + if ( int(new_ingredient.amount) > 1 ) + new_ingredient.units.plural = unit; + else + new_ingredient.units.name = unit; + } + + //unit/name separator + if ( string[ 10 ] != ' ' || string[ 11 ] == ' ' ) + return false; + + //name and preparation method + new_ingredient.name = string.mid( 11, 41 ).stripWhiteSpace(); + + //put in the header... it there is no header, current_header will be TQString::null + new_ingredient.group = current_header; + + bool last_is_sub = is_sub; + if ( new_ingredient.name.endsWith(", or") ) { + new_ingredient.name = new_ingredient.name.left(new_ingredient.name.length()-4); + is_sub = true; + } + else + is_sub = false; + + if ( last_is_sub ) + ( *list.at( list.count() - 1 ) ).substitutes.append(new_ingredient); + else + list.append( new_ingredient ); + + //if we made it this far it is an ingredient line + return true; +} + +bool MMFImporter::loadIngredientHeader( const TQString &string ) +{ + if ( ( string.startsWith( "-----" ) || string.startsWith( "MMMMM" ) ) && + string.length() >= 40 && + ( ( string.at( string.length() / 2 ) != "-" ) || + ( string.at( string.length() / 2 + 1 ) != "-" ) || + ( string.at( string.length() / 2 - 1 ) != "-" ) ) ) { + TQString header( string.stripWhiteSpace() ); + + //get only the header name + header.remove( TQRegExp( "^MMMMM" ) ); + header.remove( TQRegExp( "^-*" ) ).remove( TQRegExp( "-*$" ) ); + + kdDebug() << "found ingredient header: " << header << endl; + + //merge all columns before appending to full ingredient list to maintain the ingredient order + for ( IngredientList::iterator ing_it = m_left_col_ing.begin(); ing_it != m_left_col_ing.end(); ++ing_it ) { + m_all_ing.append( *ing_it ); + } + m_left_col_ing.empty(); + + for ( IngredientList::iterator ing_it = m_right_col_ing.begin(); ing_it != m_right_col_ing.end(); ++ing_it ) { + m_all_ing.append( *ing_it ); + } + m_right_col_ing.empty(); + + current_header = header; + return true; + } + else + return false; +} + +void MMFImporter::putDataInRecipe() +{ + for ( IngredientList::const_iterator ing_it = m_left_col_ing.begin(); ing_it != m_left_col_ing.end(); ++ing_it ) + m_all_ing.append( *ing_it ); + for ( IngredientList::const_iterator ing_it = m_right_col_ing.begin(); ing_it != m_right_col_ing.end(); ++ing_it ) + m_all_ing.append( *ing_it ); + + for ( IngredientList::iterator ing_it = m_all_ing.begin(); ing_it != m_all_ing.end(); ++ing_it ) { + TQString name_and_prep = ( *ing_it ).name; + int separator_index = name_and_prep.find( TQRegExp( "(;|,)" ) ); + if ( separator_index != -1 ) { + ( *ing_it ).name = name_and_prep.mid( 0, separator_index ).stripWhiteSpace(); + ( *ing_it ).prepMethodList = ElementList::split(",",name_and_prep.mid( separator_index + 1, name_and_prep.length() ).stripWhiteSpace() ); + } + } + + //create the recipe + Recipe new_recipe; + new_recipe.yield.amount = m_servings; + new_recipe.yield.type = i18n("servings"); + new_recipe.title = m_title; + new_recipe.instructions = m_instructions; + new_recipe.ingList = m_all_ing; + new_recipe.categoryList = m_categories; + new_recipe.authorList = m_authors; + new_recipe.recipeID = -1; + + //put it in the recipe list + add + ( new_recipe ); + + //reset for the next recipe to use these variables + resetVars(); +} + +void MMFImporter::resetVars() +{ + m_left_col_ing.empty(); + m_right_col_ing.empty(); + m_all_ing.empty(); + m_authors.clear(); + m_categories.clear(); + + m_servings = 0; + + m_title = TQString::null; + m_instructions = TQString::null; + + current_header = TQString::null; +} + diff --git a/src/importers/mmfimporter.h b/src/importers/mmfimporter.h new file mode 100644 index 0000000..8e7d0d2 --- /dev/null +++ b/src/importers/mmfimporter.h @@ -0,0 +1,65 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef MMFIMPORTER_H +#define MMFIMPORTER_H + +#include <tqstring.h> + +#include "baseimporter.h" +#include "datablocks/ingredientlist.h" +#include "datablocks/elementlist.h" + +/** Class to import Meal-Master's MMF (Meal-Master Format) file format. + * @author Jason Kivlighn + */ +class MMFImporter : public BaseImporter +{ +public: + MMFImporter(); + virtual ~MMFImporter(); + +private: + enum FormatVersion { FromDatabase, VersionMMMMM, VersionBB, VersionNormal }; + + virtual void parseFile( const TQString &filename ); + + void importMMF( TQTextStream &stream ); + + /** Parses the line and save it if the line is a valid ingredient and return true. + * Returns false if not an ingredient. + */ + bool loadIngredientLine( const TQString &, IngredientList &, bool &is_sub ); + + /** Parses the line and save it if the line is a valid ingredient header and return true. + * Returns false if not an ingredient header. + */ + bool loadIngredientHeader( const TQString & ); + + void resetVars(); + void putDataInRecipe(); + + int m_servings; + + TQString m_instructions; + TQString m_title; + + ElementList m_authors; + ElementList m_categories; + IngredientList m_left_col_ing; + IngredientList m_right_col_ing; + IngredientList m_all_ing; + + FormatVersion version; + + TQString current_header; +}; + +#endif //MMFIMPORTER_H diff --git a/src/importers/mx2importer.cpp b/src/importers/mx2importer.cpp new file mode 100644 index 0000000..75f75a7 --- /dev/null +++ b/src/importers/mx2importer.cpp @@ -0,0 +1,186 @@ +/* +Copyright (C) 2003 Richard L�rk�ng +Copyright (C) 2003 Jason Kivlighn + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307 USA +*/ + +#include "mx2importer.h" + +#include <tdelocale.h> +#include <kdebug.h> + +#include <tqfile.h> +#include <tqstringlist.h> +#include <tqtextstream.h> +#include <tqdatetime.h> + +#include "datablocks/recipe.h" + + +MX2Importer::MX2Importer() +{} + +void MX2Importer::parseFile( const TQString& filename ) +{ + TQFile file( filename ); + kdDebug() << "loading file: " << filename << endl; + if ( file.open( IO_ReadOnly ) ) { + kdDebug() << "file opened" << endl; + TQDomDocument doc; + + //hopefully a temporary hack, since MasterCook creates invalid xml declarations + TQTextStream stream( &file ); + TQString all_data = stream.read(); + if ( all_data.startsWith( "<?xml" ) ) + all_data.remove( 0, all_data.find( "?>" ) + 2 ); + + TQString error; + int line; + int column; + if ( !doc.setContent( all_data, &error, &line, &column ) ) { + kdDebug() << TQString( "error: \"%1\" at line %2, column %3" ).arg( error ).arg( line ).arg( column ) << endl; + setErrorMsg( TQString( i18n( "\"%1\" at line %2, column %3. This may not be a *.mx2 file." ) ).arg( error ).arg( line ).arg( column ) ); + return ; + } + + TQDomElement mx2 = doc.documentElement(); + + // TODO Check if there are changes between versions + if ( mx2.tagName() != "mx2" /*|| mx2.attribute("source") != "MasterCook 5.0"*/ ) { + setErrorMsg( i18n( "This file does not appear to be a *.mx2 file" ) ); + return ; + } + + TQDomNodeList l = mx2.childNodes(); + + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + + if ( el.tagName() == "RcpE" ) { + Recipe recipe; + recipe.title = el.attribute( "name" ); + + Element author( el.attribute( "author" ) ); + recipe.authorList.append( author ); + + readRecipe( el.childNodes(), &recipe ); + add + ( recipe ); + } + } + } + else + setErrorMsg( i18n( "Unable to open file." ) ); +} + +MX2Importer::~MX2Importer() +{ +} + +void MX2Importer::readRecipe( const TQDomNodeList& l, Recipe *recipe ) +{ + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + + TQString tagName = el.tagName(); + if ( tagName == "Serv" ) { + recipe->yield.amount = el.attribute( "qty" ).toInt(); + recipe->yield.type = i18n("servings"); + } + else if ( tagName == "PrpT" ) + recipe->prepTime = TQTime::fromString( el.attribute( "elapsed" ) ); + else if ( tagName == "CatS" ) { + TQDomNodeList categories = el.childNodes(); + for ( unsigned j = 0; j < categories.count(); j++ ) { + TQDomElement c = categories.item( j ).toElement(); + if ( c.tagName() == "CatT" ) { + if ( c.text().length() > 0 ) { + Element cat( c.text().stripWhiteSpace() ); + recipe->categoryList.append( cat ); + } + } + } + } + else if ( tagName == "IngR" ) { + Ingredient new_ing( el.attribute( "name" ), + el.attribute( "qty" ).toDouble(), + Unit( el.attribute( "unit" ), el.attribute( "qty" ).toDouble() ) ); + if ( el.hasChildNodes() ) { + TQDomNodeList iChilds = el.childNodes(); + for ( unsigned j = 0; j < iChilds.count(); j++ ) { + TQDomElement iChild = iChilds.item( j ).toElement(); + if ( iChild.tagName() == "IPrp" ) + new_ing.prepMethodList.append( Element(iChild.text().stripWhiteSpace()) ); + else if ( iChild.tagName() == "INtI" ) + ; // TODO: What does it mean?... ingredient nutrient info? + } + } + recipe->ingList.append( new_ing ); + } + else if ( tagName == "DirS" ) { + TQStringList directions; + TQDomNodeList dirs = el.childNodes(); + for ( unsigned j = 0; j < dirs.count(); j++ ) { + TQDomElement dir = dirs.item( j ).toElement(); + if ( dir.tagName() == "DirT" ) + directions.append( dir.text().stripWhiteSpace() ); + } + TQString directionsText; + + // TODO This is copied from RecipeML, maybe a TQStringList + // for directions in Recipe instead? + if ( directions.count() > 1 ) { + for ( unsigned i = 1; i <= directions.count(); i++ ) { + if ( i != 1 ) { + directionsText += "\n\n"; + } + + TQString sWith = TQString( "%1. " ).arg( i ); + TQString text = directions[ i - 1 ]; + if ( !text.stripWhiteSpace().startsWith( sWith ) ) + directionsText += sWith; + directionsText += text; + } + } + else + directionsText = directions[ 0 ]; + + recipe->instructions = directionsText; + } + else if ( tagName == "SrvI" ) { + // Don't know what to do with it, for now add it to directions + // btw lets hope this is read after the directions + recipe->instructions += "\n\n" + el.text().stripWhiteSpace(); + } + else if ( tagName == "Note" ) { + // Don't know what to do with it, for now add it to directions + // btw lets hope this is read after the directions + recipe->instructions += "\n\n" + el.text().stripWhiteSpace(); + } + else if ( tagName == "Nutr" ) { + //example: <Nutr>Per Serving (excluding unknown items): 51 Calories; 6g Fat (99.5% calories from fat); trace Protein; trace Carbohydrate; 0g Dietary Fiber; 16mg Cholesterol; 137mg Sodium. Exchanges: 1 Fat.</Nutr> + // Don't know what to do with it, for now add it to directions + // btw lets hope this is read after the directions + recipe->instructions += "\n\n" + el.text().stripWhiteSpace(); + } + /* tags to check for (example follows: + <Srce>SARA'S SECRETS with Sara Moulton - (Show # SS-1B43) - from the TV FOOD NETWORK</Srce> + <AltS label="Formatted for MC7" source="07-11-2003 by Joe Comiskey - Mad's Recipe Emporium"/> + */ + // TODO Have i missed some tag? + } +} + diff --git a/src/importers/mx2importer.h b/src/importers/mx2importer.h new file mode 100644 index 0000000..df298dd --- /dev/null +++ b/src/importers/mx2importer.h @@ -0,0 +1,46 @@ +/* +Copyright (C) 2003 Richard L�rk�ng +Copyright (C) 2003 Jason Kivlighn + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307 USA +*/ + +#ifndef MX2IMPORTER_H +#define MX2IMPORTER_H + +#include "baseimporter.h" + +#include <tqdom.h> + +class Recipe; + +/** Class to import Mastercook's MX2 file format. This is an XML-based file + * format used since version 5 of Mastercook. + * @author Jason Kivlighn + */ +class MX2Importer : public BaseImporter +{ +public: + MX2Importer(); + virtual ~MX2Importer(); + +protected: + void parseFile( const TQString& filename ); + +private: + void readRecipe( const TQDomNodeList& l, Recipe* ); +} ; + +#endif //MX2IMPORTER_H diff --git a/src/importers/mxpimporter.cpp b/src/importers/mxpimporter.cpp new file mode 100644 index 0000000..20fbf0f --- /dev/null +++ b/src/importers/mxpimporter.cpp @@ -0,0 +1,382 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "mxpimporter.h" + +#include <tqfile.h> +#include <tqtextstream.h> +#include <tqstringlist.h> +#include <tqdatetime.h> + +#include <tdeapplication.h> +#include <tdelocale.h> +#include <kdebug.h> + +#include "datablocks/mixednumber.h" +#include "datablocks/recipe.h" + +MXPImporter::MXPImporter() : BaseImporter() +{} + +void MXPImporter::parseFile( const TQString &file ) +{ + TQFile input( file ); + + if ( input.open( IO_ReadOnly ) ) { + TQTextStream stream( &input ); + stream.skipWhiteSpace(); + + TQString line; + while ( !stream.atEnd() ) { + line = stream.readLine().stripWhiteSpace(); + + if ( line.simplifyWhiteSpace().contains( "Exported from MasterCook" ) ) { + importMXP( stream ); + } + else if ( line == "{ Exported from MasterCook Mac }" ) { + importMac( stream ); + } + else if ( line == "@@@@@" ) { + importGeneric( stream ); + } + + stream.skipWhiteSpace(); + } + + if ( fileRecipeCount() == 0 ) + addWarningMsg( i18n( "No recipes found in this file." ) ); + } + else + setErrorMsg( i18n( "Unable to open file." ) ); +} + +MXPImporter::~MXPImporter() +{} + +void MXPImporter::importMXP( TQTextStream &stream ) +{ + Recipe recipe; + + kapp->processEvents(); //don't want the user to think its frozen... especially for files with thousands of recipes + + //kdDebug()<<"Found recipe MXP format: * Exported from MasterCook *"<<endl; + TQString current; + + // title + stream.skipWhiteSpace(); + recipe.title = stream.readLine().stripWhiteSpace(); + //kdDebug()<<"Found title: "<<m_title<<endl; + + //author + stream.skipWhiteSpace(); + current = stream.readLine().stripWhiteSpace(); + if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "recipe by" ) { + Element new_author( current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace() ); + recipe.authorList.append( new_author ); + //kdDebug()<<"Found author: "<<new_author.name<<endl; + } + else { + addWarningMsg( TQString( i18n( "While loading recipe \"%1\" " + "the field \"Recipe By:\" is either missing or could not be detected." ) ).arg( recipe.title ) ); + } + + //servings + stream.skipWhiteSpace(); + current = stream.readLine().stripWhiteSpace(); + if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "serving size" ) { + //allows serving size to be loaded even if preparation time is missing + int end_index; + if ( current.contains( "preparation time", FALSE ) ) + end_index = current.find( "preparation time", 0, FALSE ) - 15; + else + end_index = current.length(); + + recipe.yield.amount = current.mid( current.find( ":" ) + 1, end_index ).stripWhiteSpace().toInt(); + recipe.yield.type = i18n("servings"); + //kdDebug()<<"Found serving size: "<<recipe.yield.amount<<endl; + } + else { + addWarningMsg( TQString( i18n( "While loading recipe \"%1\" " + "the field \"Serving Size:\" is either missing or could not be detected." ) ).arg( recipe.title ) ); + } + + if ( current.contains( "preparation time", FALSE ) ) { + TQString prep_time = current.mid( current.find( ":", current.find( "preparation time", 0, FALSE ) ) + 1, + current.length() ).stripWhiteSpace(); + recipe.prepTime = TQTime( prep_time.section( ':', 0, 0 ).toInt(), prep_time.section( ':', 1, 1 ).toInt() ); + kdDebug() << "Found preparation time: " << prep_time << endl; + } + else { + addWarningMsg( TQString( i18n( "While loading recipe \"%1\" " + "the field \"Preparation Time:\" is either missing or could not be detected." ) ).arg( recipe.title ) ); + } + + loadCategories( stream, recipe ); + loadIngredients( stream, recipe ); + loadInstructions( stream, recipe ); + loadOptionalFields( stream, recipe ); + + add + ( recipe ); + + if ( !stream.atEnd() ) { + importMXP( stream ); + return ; + } +} + +void MXPImporter::loadCategories( TQTextStream &stream, Recipe &recipe ) +{ + //====================categories====================// + stream.skipWhiteSpace(); + TQString current = stream.readLine().stripWhiteSpace(); + if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "categories" ) { + TQString tmp_str = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + + while ( current.stripWhiteSpace() != "Amount Measure Ingredient -- Preparation Method" && !stream.atEnd() ) { + if ( !tmp_str.isEmpty() ) { + TQStringList categories = TQStringList::split( " ", tmp_str ); + for ( TQStringList::const_iterator it = categories.begin(); it != categories.end(); ++it ) { + Element new_cat( ( *it ).stripWhiteSpace() ); + recipe.categoryList.append( new_cat ); + + //kdDebug()<<"Found category: "<<new_cat.name<<endl; + } + } + + current = stream.readLine(); + tmp_str = current; + } + //else + // kdDebug()<<"No categories found."<<endl; + } + else { + addWarningMsg( TQString( i18n( "While loading recipe \"%1\" " + "the field \"Categories:\" is either missing or could not be detected." ) ).arg( recipe.title ) ); + + //the ingredient loaded will expect the last thing to have been read to be this header line + while ( current.stripWhiteSpace() != "Amount Measure Ingredient -- Preparation Method" && !stream.atEnd() ) + current = stream.readLine(); + } +} + +void MXPImporter::loadIngredients( TQTextStream &stream, Recipe &recipe ) +{ + //============ingredients=================// + stream.skipWhiteSpace(); + ( void ) stream.readLine(); + TQString current = stream.readLine(); + if ( !current.contains( "NONE" ) && !current.isEmpty() ) { + while ( !current.isEmpty() && !stream.atEnd() ) { + Ingredient new_ingredient; + + //amount + TQString amount_str = current.mid( 0, 9 ).simplifyWhiteSpace(); + if ( !amount_str.isEmpty() ) // case of amount_str.isEmpty() correctly handled by class default + { + bool ok; + MixedNumber amount( MixedNumber::fromString( amount_str, &ok ) ); + if ( !ok ) + { + addWarningMsg( TQString( i18n( "While loading recipe \"%1\" Invalid amount \"%2\" in the line \"%3\"" ) ).arg( recipe.title ).arg( amount_str ).arg( current.stripWhiteSpace() ) ); + current = stream.readLine(); + continue; + } + new_ingredient.amount = amount.toDouble(); + } + + //units + TQString units( current.mid( 9, 13 ) ); + new_ingredient.units = Unit( units.simplifyWhiteSpace(), new_ingredient.amount ); + + //name + int dash_index = current.find( "--" ); + + int length; + if ( dash_index == -1 || dash_index == 24 ) //ignore a dash in the first position (index 24) + length = current.length(); + else + length = dash_index - 22; + + TQString ingredient_name( current.mid( 22, length ) ); + new_ingredient.name = ingredient_name.stripWhiteSpace(); + + //prep method + if ( dash_index != -1 && dash_index != 24 ) //ignore a dash in the first position (index 24) + new_ingredient.prepMethodList.append( Element(current.mid( dash_index + 2, current.length() ).stripWhiteSpace()) ); + + recipe.ingList.append( new_ingredient ); + //kdDebug()<<"Found ingredient: amount="<<new_ingredient.amount + // <<", unit:"<<new_ingredient.units + // <<", name:"<<new_ingredient.name + // <<", prep_method:"<<prep_method<<endl; + + current = stream.readLine(); + } + } + //else + // kdDebug()<<"No ingredients found."<<endl; +} + +void MXPImporter::loadInstructions( TQTextStream &stream, Recipe &recipe ) +{ + //==========================instructions ( along with other optional fields... mxp format doesn't define end of ingredients and start of other fields )==============// + stream.skipWhiteSpace(); + TQString current = stream.readLine().stripWhiteSpace(); + while ( !current.contains( "- - - -" ) && !stream.atEnd() ) { + if ( current.stripWhiteSpace() == "Source:" ) { + Element new_author( getNextQuotedString( stream ) ); + recipe.authorList.append( new_author ); + //kdDebug()<<"Found source: "<<new_author.name<<" (adding as author)"<<endl; + } + else if ( current.stripWhiteSpace() == "Description:" ) { + TQString description = getNextQuotedString( stream ); + //kdDebug()<<"Found description: "<<m_description<<" (adding to end of instructions)"<<endl; + recipe.instructions += "\n\nDescription: " + description; + } + else if ( current.stripWhiteSpace() == "S(Internet Address):" ) { + TQString internet = getNextQuotedString( stream ); + //kdDebug()<<"Found internet address: "<<m_internet<<" (adding to end of instructions)"<<endl; + recipe.instructions += "\n\nInternet address: " + internet; + } + else if ( current.stripWhiteSpace() == "Yield:" ) { + recipe.yield.amount = getNextQuotedString( stream ).stripWhiteSpace().toInt(); + recipe.yield.type = i18n("servings"); + //kdDebug()<<"Found yield: "<<recipe.yield.amount<<" (adding as servings)"<<endl; + } + else if ( current.stripWhiteSpace() == "T(Cook Time):" ) { + ( void ) getNextQuotedString( stream ); //this would be prep time, but we don't use prep time at the moment + //kdDebug()<<"Found cook time: "<<m_prep_time<<" (adding as prep time)"<<endl; + } + else if ( current.stripWhiteSpace() == "Cuisine:" ) { + Element new_cat( getNextQuotedString( stream ) ); + recipe.categoryList.append( new_cat ); + //kdDebug()<<"Found cuisine (adding as category): "<<new_cat.name<<endl; + } + else + recipe.instructions += current + "\n"; + + current = stream.readLine().stripWhiteSpace(); + } + recipe.instructions = recipe.instructions.stripWhiteSpace(); + //kdDebug()<<"Found instructions: "<<m_instructions<<endl; +} + +void MXPImporter::loadOptionalFields( TQTextStream &stream, Recipe &recipe ) +{ + //=================after here, fields are optional=========================// + stream.skipWhiteSpace(); + TQString current = stream.readLine().stripWhiteSpace(); + + TQString notes; + + //Note: we simplifyWhiteSpace() because some versions of MasterCook have "Exported from MasterCook" and others have "Exported from MasterCook". + // This also could work around a typo or such. + while ( !current.simplifyWhiteSpace().contains( "Exported from MasterCook" ) && !stream.atEnd() ) { + //suggested wine + if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "suggested wine" ) { + TQString wine = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + //kdDebug()<<"Found suggested wine: "<<m_wine<<" (adding to end of instructions)"<<endl; + + recipe.instructions += "\n\nSuggested wine: " + wine; + } + //Nutr. Assoc. + if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "nutr. assoc." ) { + TQString nutr_assoc = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + //kdDebug()<<"Found nutrient association: "<<nutr_assoc<<" (adding to end of instructions)"<<endl; + + recipe.instructions += "\n\nNutrient Association: " + nutr_assoc; + } + else if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "per serving (excluding unknown items)" ) { //per serving... maybe we can do something with this info later + TQString per_serving_info = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + //kdDebug()<<"Found per serving (excluding unknown items): "<<per_serving_info<<" (adding to end of instructions)"<<endl; + + recipe.instructions += "\n\nPer Serving (excluding unknown items): " + per_serving_info; + } + else if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "per serving" ) { //per serving... maybe we can do something with this info later + TQString per_serving_info = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + //kdDebug()<<"Found per serving: "<<per_serving_info<<" (adding to end of instructions)"<<endl; + + recipe.instructions += "\n\nPer Serving: " + per_serving_info; + } + else if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "food exchanges" ) { //food exchanges... maybe we can do something with this info later + TQString food_exchange_info = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + //kdDebug()<<"Found food exchanges: "<<food_exchange_info<<" (adding to end of instructions)"<<endl; + + recipe.instructions += "\n\nFood Exchanges: " + food_exchange_info; + } + else if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "serving ideas" ) { //serving ideas + TQString serving_ideas = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + //kdDebug()<<"Found serving ideas: "<<m_serving_ideas<<" (adding to end of instructions)"<<endl; + + recipe.instructions += "\n\nServing ideas: " + serving_ideas; + } + else if ( current.mid( 0, current.find( ":" ) ).simplifyWhiteSpace().lower() == "notes" ) //notes + notes = current.mid( current.find( ":" ) + 1, current.length() ).stripWhiteSpace(); + else if ( !current.isEmpty() && current != "_____" ) //if it doesn't belong to any other field, assume it a part of a multi-line notes field + notes += "\n" + current; + + current = stream.readLine().stripWhiteSpace(); + } + + /*possible fields to implement later: + + Nutr. Assoc. : 0 0 0 0 0 + + Ratings : Cholesterol Rating 5 Complete Meal 3 + Cost 3 Depth 3 + Difficulty 2 Fanciness 7 + Fat Content 5 Good For Crowds 10 + Intensity 5 Intricacy 2 + Kid Appeal 3 Looks 5 + Portability 3 Richness 7 + Serving Temperature 8 Spicy Hotness 2 + Tartness 7 + + */ + if ( !notes.isNull() ) { + //kdDebug()<<TQString("Found notes: %s (adding to end of instructions)").arg(m_notes)<<endl; + recipe.instructions += "\n\nNotes: " + notes.stripWhiteSpace(); + } +} + +void MXPImporter::importGeneric( TQTextStream & /*stream*/ ) +{ + setErrorMsg( i18n( "MasterCook's Generic Export format is currently not supported. Please write to [email protected] to request support for this format." ) ); + //not even sure it this is worth writing... its rather obsolete +} + +void MXPImporter::importMac( TQTextStream & /*stream*/ ) +{ + setErrorMsg( i18n( "MasterCook Mac's Export format is currently not supported. Please write to [email protected] to request support for this format." ) ); + //not even sure it this is worth writing... its rather obsolete +} + +TQString MXPImporter::getNextQuotedString( TQTextStream &stream ) +{ + stream.skipWhiteSpace(); + TQString current = stream.readLine().stripWhiteSpace(); + TQString return_str; + + if ( current.left( 1 ) == "\"" ) + return_str = current.mid( 1, current.length() - 1 ); + else + return current; + + while ( current.right( 1 ) != "\"" && !stream.atEnd() ) { + current = stream.readLine().stripWhiteSpace(); + return_str += "\n" + current; + } + + //take off quote at end + return_str = return_str.mid( 0, return_str.length() - 1 ); + + return return_str.stripWhiteSpace(); +} diff --git a/src/importers/mxpimporter.h b/src/importers/mxpimporter.h new file mode 100644 index 0000000..6bdee22 --- /dev/null +++ b/src/importers/mxpimporter.h @@ -0,0 +1,45 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef MXPIMPORTER_H +#define MXPIMPORTER_H + +#include <tqstring.h> + +#include "baseimporter.h" + +/** Class to import MasterCook's MXP (MasterCook Export) file format. + * This is a human-readable format used in Mastercook up until version 4. + * @author Jason Kivlighn + */ +class MXPImporter : public BaseImporter +{ +public: + MXPImporter(); + virtual ~MXPImporter(); + +protected: + void parseFile( const TQString& filename ); + +private: + void importMXP( TQTextStream &stream ); + + void loadCategories( TQTextStream &stream, Recipe &recipe ); + void loadIngredients( TQTextStream &stream, Recipe &recipe ); + void loadInstructions( TQTextStream &stream, Recipe &recipe ); + void loadOptionalFields( TQTextStream &stream, Recipe &recipe ); + + void importMac( TQTextStream &stream ); + void importGeneric( TQTextStream &stream ); + + TQString getNextQuotedString( TQTextStream &stream ); +}; + +#endif //MXPIMPORTER_H diff --git a/src/importers/nycgenericimporter.cpp b/src/importers/nycgenericimporter.cpp new file mode 100644 index 0000000..30e5bdf --- /dev/null +++ b/src/importers/nycgenericimporter.cpp @@ -0,0 +1,196 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "nycgenericimporter.h" + +#include <tdeapplication.h> +#include <tdelocale.h> +#include <kdebug.h> + +#include <tqfile.h> +#include <tqtextstream.h> +#include <tqstringlist.h> +#include <tqregexp.h> + +#include "datablocks/mixednumber.h" +#include "datablocks/recipe.h" + +NYCGenericImporter::NYCGenericImporter() : BaseImporter() +{} + +void NYCGenericImporter::parseFile( const TQString &file ) +{ + first = true; + + m_recipe.empty(); + + TQFile input( file ); + if ( input.open( IO_ReadOnly ) ) { + TQTextStream stream( &input ); + stream.skipWhiteSpace(); + + if ( !stream.atEnd() && stream.readLine().startsWith( "@@@@@" ) ) + importNYCGeneric( stream ); + else { + setErrorMsg( i18n( "File does not appear to be a valid NYC export." ) ); + return ; + } + } + else + setErrorMsg( i18n( "Unable to open file." ) ); +} + +NYCGenericImporter::~NYCGenericImporter() +{} + +void NYCGenericImporter::importNYCGeneric( TQTextStream &stream ) +{ + kapp->processEvents(); //don't want the user to think its frozen... especially for files with thousands of recipes + + TQString current; + + stream.skipWhiteSpace(); + + //title + while ( !( current = stream.readLine() ).isEmpty() && !stream.atEnd() ) + m_recipe.title = current; + + //categories + while ( !( current = stream.readLine() ).isEmpty() && !stream.atEnd() ) { + if ( current[ 0 ].isNumber() ) { + loadIngredientLine( current ); + break; + } //oops, this is really an ingredient line (there was no category line) + + TQStringList categories = TQStringList::split( ',', current ); + + if ( categories.count() > 0 && categories[ 0 ].upper() == "none" ) //there are no categories + break; + + for ( TQStringList::const_iterator it = categories.begin(); it != categories.end(); ++it ) { + Element new_cat( TQString( *it ).stripWhiteSpace() ); + kdDebug() << "Found category: " << new_cat.name << endl; + m_recipe.categoryList.append( new_cat ); + } + } + + //ingredients + while ( !( current = stream.readLine() ).isEmpty() && !stream.atEnd() ) + loadIngredientLine( current ); + + //everything else is the instructions with optional "contributor", "prep time" and "yield" + bool found_next; + while ( !( found_next = ( current = stream.readLine() ).startsWith( "@@@@@" ) ) && !stream.atEnd() ) { + if ( current.startsWith( "Contributor:" ) ) { + Element new_author( current.mid( current.find( ':' ) + 1, current.length() ).stripWhiteSpace() ); + kdDebug() << "Found author: " << new_author.name << endl; + m_recipe.authorList.append( new_author ); + } + else if ( current.startsWith( "Preparation Time:" ) ) { + m_recipe.prepTime = TQTime::fromString( current.mid( current.find( ':' ), current.length() ) ); + } + else if ( current.startsWith( "Yield:" ) ) { + int colon_index = current.find( ':' ); + int amount_type_sep_index = current.find(" ",colon_index+1); + + m_recipe.yield.amount = current.mid( colon_index+2, amount_type_sep_index-colon_index ).toDouble(); + m_recipe.yield.type = current.mid( amount_type_sep_index+3, current.length() ); + } + else if ( current.startsWith( "NYC Nutrition Analysis (per serving or yield unit):" ) ) { + //m_recipe.instructions += current + "\n"; + } + else if ( current.startsWith( "NYC Nutrilink:" ) ) { + //m_recipe.instructions += current + "\n"; + } + else if ( !current.stripWhiteSpace().isEmpty() && !current.startsWith("** Exported from Now You're Cooking!") ) { + m_recipe.instructions += current + "\n"; + } + } + + m_recipe.instructions = m_recipe.instructions.stripWhiteSpace(); + putDataInRecipe(); + + if ( found_next ) + importNYCGeneric( stream ); +} + +void NYCGenericImporter::putDataInRecipe() +{ + //put it in the recipe list + add( m_recipe ); + + //reset for the next recipe + m_recipe.empty(); +} + +void NYCGenericImporter::loadIngredientLine( const TQString &line ) +{ + TQString current = line; + + if ( current.contains( "-----" ) ) { + current_header = current.stripWhiteSpace(); + kdDebug() << "Found ingredient header: " << current_header << endl; + return ; + } + + MixedNumber amount( 0, 0, 1 ); + TQString unit; + TQString name; + TQString prep; + + TQStringList ingredient_line = TQStringList::split( ' ', current ); + + bool found_amount = false; + + if ( !ingredient_line.empty() ) //probably an unnecessary check... but to be safe + { + bool ok; + MixedNumber test_amount = MixedNumber::fromString( ingredient_line[ 0 ], &ok ); + if ( ok ) + { + amount = amount + test_amount; + ingredient_line.pop_front(); + found_amount = true; + } + } + if ( !ingredient_line.empty() ) //probably an unnecessary check... but to be safe + { + bool ok; + MixedNumber test_amount = MixedNumber::fromString( ingredient_line[ 0 ], &ok ); + if ( ok ) + { + amount = amount + test_amount; + ingredient_line.pop_front(); + found_amount = true; + } + } + + if ( found_amount ) { + unit = ingredient_line[ 0 ]; + ingredient_line.pop_front(); + } + + //now join each separate part of ingredient (name, unit, amount) + name = ingredient_line.join( " " ); + + int prep_sep_index = name.find( TQRegExp( "(--|,;;)" ) ); + if ( prep_sep_index == -1 ) + prep_sep_index = name.length(); + + name = name.left( prep_sep_index ).stripWhiteSpace(); + prep = name.mid( prep_sep_index+1, name.length() ).stripWhiteSpace(); + + Ingredient new_ingredient( name, amount.toDouble(), Unit( unit, amount.toDouble() ) ); + new_ingredient.group = current_header; + new_ingredient.prepMethodList = ElementList::split(",",prep); + m_recipe.ingList.append( new_ingredient ); + +} + diff --git a/src/importers/nycgenericimporter.h b/src/importers/nycgenericimporter.h new file mode 100644 index 0000000..6b3bc2f --- /dev/null +++ b/src/importers/nycgenericimporter.h @@ -0,0 +1,44 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef NYCGENERICIMPORTER_H +#define NYCGENERICIMPORTER_H + +#include <tqstring.h> +#include <tqdatetime.h> + +#include "baseimporter.h" +#include "datablocks/ingredientlist.h" +#include "datablocks/elementlist.h" + +/** Class to import The NYC (Now You're Cooking) Generic Export file format. + * @author Jason Kivlighn + */ +class NYCGenericImporter : public BaseImporter +{ +public: + NYCGenericImporter(); + ~NYCGenericImporter(); + +protected: + void parseFile( const TQString& filename ); + +private: + void importNYCGeneric( TQTextStream &stream ); + void putDataInRecipe(); + void loadIngredientLine( const TQString & ); + + Recipe m_recipe; + TQString current_header; + + bool first; +}; + +#endif //NYCGENERICIMPORTER_H diff --git a/src/importers/recipemlimporter.cpp b/src/importers/recipemlimporter.cpp new file mode 100644 index 0000000..8866648 --- /dev/null +++ b/src/importers/recipemlimporter.cpp @@ -0,0 +1,376 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Richard L�rk�ng * +* * +* Copyright (C) 2003-2005 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "recipemlimporter.h" + +#include <tqfile.h> +#include <tqdatetime.h> + +#include <tdelocale.h> +#include <kdebug.h> + +#include "datablocks/recipe.h" +#include "datablocks/mixednumber.h" + +RecipeMLImporter::RecipeMLImporter() : BaseImporter() +{} + +void RecipeMLImporter::parseFile( const TQString& file ) +{ + TQFile input( file ); + if ( input.open( IO_ReadOnly ) ) { + TQDomDocument doc; + TQString error; + int line; + int column; + if ( !doc.setContent( &input, &error, &line, &column ) ) { + setErrorMsg( TQString( i18n( "\"%1\" at line %2, column %3. This may not be a RecipeML file." ) ).arg( error ).arg( line ).arg( column ) ); + return ; + } + + TQDomElement recipeml = doc.documentElement(); + + if ( recipeml.tagName() != "recipeml" ) { + setErrorMsg( i18n( "This file does not appear to be a valid RecipeML archive." ) ); + return ; + } + + TQDomNodeList l = recipeml.childNodes(); + + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + TQString tagName = el.tagName(); + + if ( tagName == "meta" ) + continue; + else if ( tagName == "recipe" ) + readRecipemlRecipe( el ); + else if ( tagName == "menu" ) + readRecipemlMenu( el ); + else + kdDebug() << "Unknown tag within <recipeml>: " << tagName << endl; + } + } + else + setErrorMsg( i18n( "Unable to open file." ) ); +} + +RecipeMLImporter::~RecipeMLImporter() +{} + +void RecipeMLImporter::readRecipemlRecipe( const TQDomElement& recipe_element ) +{ + recipe.empty(); + + TQDomNodeList l = recipe_element.childNodes(); + + for ( unsigned i = 0; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + TQString tagName = el.tagName(); + + if ( tagName == "head" ) + readRecipemlHead( el ); + else if ( tagName == "ingredients" ) + readRecipemlIngs( el ); + else if ( tagName == "directions" ) + readRecipemlDirections( el ); + else if ( tagName == "description" ) {} //TODO: what do we do with this? + else if ( tagName == "equipment" ) {} //TODO: what do we do with this? + else if ( tagName == "nutrition" ) {} //TODO: what do we do with this? + else if ( tagName == "diet-exchanges" ) {} //TODO: what do we do with this? + else + kdDebug() << "Unknown tag within <recipe>: " << el.tagName() << endl; + } + + add + ( recipe ); +} + +void RecipeMLImporter::readRecipemlHead( const TQDomElement& head ) +{ + TQDomNodeList l = head.childNodes(); + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + TQString tagName = el.tagName(); + + if ( tagName == "title" ) + recipe.title = el.text().stripWhiteSpace(); + else if ( tagName == "subtitle" ) + recipe.title += ": " + el.text().stripWhiteSpace(); + else if ( tagName == "version" ) {} //TODO: what do we do with this? + else if ( tagName == "source" ) + readRecipemlSrcItems( el ); + else if ( tagName == "categories" ) { + TQDomNodeList categories = el.childNodes(); + for ( unsigned j = 0; j < categories.count(); j++ ) { + TQDomElement c = categories.item( j ).toElement(); + if ( c.tagName() == "cat" ) { + recipe.categoryList.append( Element( c.text() ) ); + } + } + } + else if ( tagName == "description" ) + recipe.instructions += "\n\nDescription: " + el.text().stripWhiteSpace(); + else if ( tagName == "preptime" ) + readRecipemlPreptime( el ); + else if ( tagName == "yield" ) { + TQDomNodeList yieldChildren = el.childNodes(); + for ( unsigned j = 0; j < yieldChildren.count(); j++ ) { + TQDomElement y = yieldChildren.item( j ).toElement(); + TQString tagName = y.tagName(); + if ( tagName == "range" ) + readRecipemlRange( y, recipe.yield.amount, recipe.yield.amount_offset ); + else if ( tagName == "unit" ) + recipe.yield.type = y.text(); + else + kdDebug() << "Unknown tag within <yield>: " << y.tagName() << endl; + } + } + else + kdDebug() << "Unknown tag within <head>: " << el.tagName() << endl; + } +} + +void RecipeMLImporter::readRecipemlIngs( const TQDomElement& ings ) +{ + TQDomNodeList l = ings.childNodes(); + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + TQString tagName = el.tagName(); + + if ( tagName == "ing" ) + readRecipemlIng( el ); + else if ( tagName == "ing-div" ) //NOTE: this can have the "type" attribute + { + TQString header; + TQDomNodeList ingDiv = el.childNodes(); + for ( unsigned j = 0; j < ingDiv.count(); j++ ) + { + TQDomElement cEl = ingDiv.item( j ).toElement(); + if ( cEl.tagName() == "title" ) + header = cEl.text().stripWhiteSpace(); + else if ( cEl.tagName() == "description" ) {} //TODO: what do we do with this? + else if ( cEl.tagName() == "ing" ) + readRecipemlIng( cEl, 0, header ); + else if ( tagName == "note" ) {} //TODO: what do we do with this? + else + kdDebug() << "Unknown tag within <ing-div>: " << cEl.tagName() << endl; + } + } + else if ( tagName == "note" ) {} //TODO: what do we do with this? + else + kdDebug() << "Unknown tag within <ingredients>: " << el.tagName() << endl; + } +} + +void RecipeMLImporter::readRecipemlIng( const TQDomElement& ing, Ingredient *ing_parent, const TQString &header ) +{ + Ingredient new_ing; + + TQDomNodeList ingChilds = ing.childNodes(); + + TQString name, unit, size, prep_method; + Ingredient quantity; + quantity.amount = 1;// default quantity assumed by RecipeML DTD + + for ( unsigned j = 0; j < ingChilds.count(); j++ ) { + TQDomElement ingChild = ingChilds.item( j ).toElement(); + TQString tagName = ingChild.tagName(); + + if ( tagName == "amt" ) { + TQDomNodeList amtChilds = ingChild.childNodes(); + + for ( unsigned k = 0; k < amtChilds.count(); k++ ) { + TQDomElement amtChild = amtChilds.item( k ).toElement(); + + if ( amtChild.tagName() == "qty" ) + readRecipemlTQty( amtChild, quantity ); + else if ( amtChild.tagName() == "size" ) + size = amtChild.text().stripWhiteSpace(); + else if ( amtChild.tagName() == "unit" ) + unit = amtChild.text().stripWhiteSpace(); + else + kdDebug() << "Unknown tag within <amt>: " << amtChild.tagName() << endl; + } + } + else if ( tagName == "item" ) { + name = ingChild.text().stripWhiteSpace(); + if ( ing.attribute( "optional", "no" ) == "yes" ) + prep_method = "(optional)"; + } + else if ( tagName == "prep" ) { //FIXME: this overwrite the optional attribute + prep_method = ingChild.text().stripWhiteSpace(); + } + else if ( tagName == "alt-ing" ) + readRecipemlIng( ingChild, &new_ing, header ); + else + kdDebug() << "Unknown tag within <ing>: " << ingChild.tagName() << endl; + } + + if ( !size.isEmpty() ) + unit.prepend( size + " " ); + + new_ing.name = name; + new_ing.units = Unit( unit, quantity.amount+quantity.amount_offset ); + new_ing.amount = quantity.amount; + new_ing.amount_offset = quantity.amount_offset; + new_ing.group = header; + new_ing.prepMethodList = ElementList::split(",",prep_method); + + if ( !ing_parent ) + recipe.ingList.append(new_ing); + else + ing_parent->substitutes.append( new_ing ); +} + +void RecipeMLImporter::readRecipemlDirections( const TQDomElement& dirs ) +{ + TQDomNodeList l = dirs.childNodes(); + + TQStringList directions; + + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + + if ( el.tagName() == "step" ) + directions.append( el.text().stripWhiteSpace() ); + else + kdDebug() << "Unknown tag within <directions>: " << el.tagName() << endl; + } + + TQString directionsText; + + if ( directions.count() > 1 ) { + for ( unsigned i = 1; i <= directions.count(); i++ ) { + if ( i != 1 ) { + directionsText += "\n\n"; + } + + TQString sWith = TQString( "%1. " ).arg( i ); + TQString text = directions[ i - 1 ]; + if ( !text.stripWhiteSpace().startsWith( sWith ) ) + directionsText += sWith; + directionsText += text; + } + } + else + directionsText = directions[ 0 ]; + + recipe.instructions = directionsText; +} + +void RecipeMLImporter::readRecipemlMenu( const TQDomElement& menu_el ) +{ + TQDomNodeList l = menu_el.childNodes(); + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + TQString tagName = el.tagName(); + + if ( tagName == "head" ) + readRecipemlHead( el ); + else if ( tagName == "description" ) {} //TODO: what do we do with this? + else if ( tagName == "recipe" ) + readRecipemlRecipe( el ); + else + kdDebug() << "Unknown tag within <menu>: " << tagName << endl; + } +} + +void RecipeMLImporter::readRecipemlSrcItems( const TQDomElement& sources ) +{ + TQDomNodeList l = sources.childNodes(); + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement srcitem = l.item( i ).toElement(); + TQString tagName = srcitem.tagName(); + + if ( tagName == "srcitem" ) + recipe.authorList.append( Element( srcitem.text().stripWhiteSpace() ) ); + else + kdDebug() << "Unknown tag within <source>: " << tagName << endl; + } +} + +void RecipeMLImporter::readRecipemlPreptime( const TQDomElement &preptime ) +{ + TQDomNodeList l = preptime.childNodes(); + for ( unsigned i = 0 ; i < l.count(); i++ ) { + TQDomElement el = l.item( i ).toElement(); + TQString tagName = el.tagName(); + + if ( tagName == "time" ) { + int qty = 0; + TQString timeunit; + + TQDomNodeList time_l = el.childNodes(); + for ( unsigned i = 0 ; i < time_l.count(); i++ ) { + TQDomElement time_el = time_l.item( i ).toElement(); + TQString time_tagName = time_el.tagName(); + + if ( time_tagName == "qty" ) + qty = time_el.text().toInt(); + else if ( time_tagName == "timeunit" ) + timeunit = time_el.text(); + else + kdDebug() << "Unknown tag within <time>: " << time_tagName << endl; + } + + int minutes = 0; + int hours = 0; + if ( timeunit == "minutes" ) + minutes = qty; + else if ( timeunit == "hours" ) + hours = qty; + else + kdDebug() << "Unknown timeunit: " << timeunit << endl; + + recipe.prepTime = TQTime( hours + minutes / 60, minutes % 60 ); + } + else + kdDebug() << "Unknown tag within <preptime>: " << tagName << endl; + } +} + +void RecipeMLImporter::readRecipemlTQty( const TQDomElement &qty, Ingredient &ing ) +{ + TQDomNodeList qtyChilds = qty.childNodes(); + + for ( unsigned i = 0; i < qtyChilds.count(); i++ ) { + TQDomElement qtyChild = qtyChilds.item( i ).toElement(); + TQString tagName = qtyChild.tagName(); + if ( tagName == "range" ) + readRecipemlRange( qtyChild, ing.amount, ing.amount_offset ); + else if ( tagName.isEmpty() ) + ing.amount = MixedNumber::fromString( qty.text() ).toDouble(); + else + kdDebug() << "Unknown tag within <qty>: " << tagName << endl; + } +} + +void RecipeMLImporter::readRecipemlRange( const TQDomElement& range, double &amount, double &amount_offset ) +{ + TQDomNodeList rangeChilds = range.childNodes(); + double q1 = 1, q2 = 0; // default quantity assumed by RecipeML DTD + for ( unsigned j = 0; j < rangeChilds.count(); j++ ) { + TQDomElement rangeChild = rangeChilds.item( j ).toElement(); + TQString subTagName = rangeChild.tagName(); + if ( subTagName == "q1" ) + q1 = MixedNumber::fromString( rangeChild.text() ).toDouble(); + else if ( subTagName == "q2" ) + q2 = MixedNumber::fromString( rangeChild.text() ).toDouble(); + else + kdDebug() << "Unknown tag within <range>: " << subTagName << endl; + } + + amount = q1; + amount_offset = q2-q1; +} diff --git a/src/importers/recipemlimporter.h b/src/importers/recipemlimporter.h new file mode 100644 index 0000000..51c6f42 --- /dev/null +++ b/src/importers/recipemlimporter.h @@ -0,0 +1,53 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Richard L�rk�ng * +* * +* Copyright (C) 2003-2005 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + + +#ifndef RECIPEMLIMPORTER_H +#define RECIPEMLIMPORTER_H + +#include "baseimporter.h" +#include "datablocks/ingredient.h" +#include "datablocks/recipe.h" + +#include <tqdom.h> + +/** Class to import the RecipeML, XML-based file format. + * More info at http://www.formatdata.com/recipeml + * + * @author Jason Kivlighn + */ +class RecipeMLImporter : public BaseImporter +{ +public: + RecipeMLImporter(); + virtual ~RecipeMLImporter(); + +protected: + void parseFile( const TQString& filename ); + +private: + void readRecipemlDirections( const TQDomElement& dirs ); + void readRecipemlHead( const TQDomElement& head ); + void readRecipemlIng( const TQDomElement& ing1, Ingredient *ing2 = 0, const TQString &header = TQString::null ); + void readRecipemlIngs( const TQDomElement& ings ); + void readRecipemlMenu( const TQDomElement& menu ); + void readRecipemlSrcItems( const TQDomElement& sources ); + void readRecipemlRecipe( const TQDomElement& recipe ); + void readRecipemlPreptime( const TQDomElement &preptime ); + void readRecipemlTQty( const TQDomElement &qty, Ingredient &ing ); + void readRecipemlRange( const TQDomElement& range1, double &range2, double &range_offset ); + + Recipe recipe; +}; + +#endif //RECIPEMLIMPORTER_H diff --git a/src/importers/rezkonvimporter.cpp b/src/importers/rezkonvimporter.cpp new file mode 100644 index 0000000..58e59c0 --- /dev/null +++ b/src/importers/rezkonvimporter.cpp @@ -0,0 +1,291 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#include "rezkonvimporter.h" + +#include <tdeapplication.h> +#include <tdelocale.h> +#include <kdebug.h> + +#include <tqfile.h> +#include <tqregexp.h> +#include <tqtextstream.h> + +#include "datablocks/mixednumber.h" + +RezkonvImporter::RezkonvImporter() : BaseImporter() +{} + +RezkonvImporter::~RezkonvImporter() +{} + +void RezkonvImporter::parseFile( const TQString &filename ) +{ + TQFile input( filename ); + + if ( input.open( IO_ReadOnly ) ) { + TQTextStream stream( &input ); + stream.skipWhiteSpace(); + + TQString line; + + while ( !stream.atEnd() ) { + line = stream.readLine(); + + if ( line.contains( TQRegExp( "^=====.*REZKONV.*" ) ) ) { + TQStringList raw_recipe; + while ( !( line = stream.readLine() ).contains( TQRegExp( "^=====\\s*$" ) ) && !stream.atEnd() ) + raw_recipe << line; + + readRecipe( raw_recipe ); + } + } + + if ( fileRecipeCount() == 0 ) + setErrorMsg( i18n( "No recipes found in this file." ) ); + } + else + setErrorMsg( i18n( "Unable to open file." ) ); +} + +void RezkonvImporter::readRecipe( const TQStringList &raw_recipe ) +{ + kapp->processEvents(); //don't want the user to think its frozen... especially for files with thousands of recipes + + Recipe recipe; + + TQStringList::const_iterator text_it = raw_recipe.begin(); + m_end_it = raw_recipe.end(); + + //title (Titel) + text_it++; + recipe.title = ( *text_it ).mid( ( *text_it ).find( ":" ) + 1, ( *text_it ).length() ).stripWhiteSpace(); + kdDebug() << "Found title: " << recipe.title << endl; + + //categories (Kategorien): + text_it++; + TQStringList categories = TQStringList::split( ',', ( *text_it ).mid( ( *text_it ).find( ":" ) + 1, ( *text_it ).length() ) ); + for ( TQStringList::const_iterator it = categories.begin(); it != categories.end(); ++it ) { + Element new_cat; + new_cat.name = TQString( *it ).stripWhiteSpace(); + kdDebug() << "Found category: " << new_cat.name << endl; + recipe.categoryList.append( new_cat ); + } + + //yield (Menge) + text_it++; + //get the number between the ":" and the next space after it + TQString yield_str = ( *text_it ).stripWhiteSpace(); + yield_str.remove( TQRegExp( "^Menge:\\s*" ) ); + int sep_index = yield_str.find( ' ' ); + if ( sep_index != -1 ) + recipe.yield.type = yield_str.mid( sep_index+1 ); + readRange( yield_str.mid( 0, sep_index ), recipe.yield.amount, recipe.yield.amount_offset ); + kdDebug() << "Found yield: " << recipe.yield.amount << endl; + + bool is_sub = false; + bool last_line_empty = false; + text_it++; + while ( text_it != raw_recipe.end() ) { + if ( ( *text_it ).isEmpty() ) { + last_line_empty = true; + text_it++; + continue; + } + + if ( ( *text_it ).contains( TQRegExp( "^=====.*=$" ) ) ) //is a header + { + if ( ( *text_it ).contains( "quelle", false ) ) + { + loadReferences( text_it, recipe ); + break; //reference lines are the last before the instructions + } + else + loadIngredientHeader( *text_it, recipe ); + } + + //if it has no more than two spaces followed by a non-digit + //then we'll assume it is a direction line + else if ( last_line_empty && ( *text_it ).contains( TQRegExp( "^\\s{0,2}[^\\d\\s=]" ) ) ) + break; + else + loadIngredient( *text_it, recipe, is_sub ); + + last_line_empty = false; + text_it++; + } + + loadInstructions( text_it, recipe ); + + add + ( recipe ); + + current_header = TQString::null; +} + +void RezkonvImporter::loadIngredient( const TQString &string, Recipe &recipe, bool &is_sub ) +{ + Ingredient new_ingredient; + new_ingredient.amount = 0; //amount not required, so give default of 0 + + TQRegExp cont_test( "^-{1,2}" ); + if ( string.stripWhiteSpace().contains( cont_test ) ) { + TQString name = string.stripWhiteSpace(); + name.remove( cont_test ); + kdDebug() << "Appending to last ingredient: " << name << endl; + if ( !recipe.ingList.isEmpty() ) //so it doesn't crash when the first ingredient appears to be a continuation of another + ( *recipe.ingList.at( recipe.ingList.count() - 1 ) ).name += " " + name; + + return ; + } + + //amount + if ( !string.mid( 0, 7 ).stripWhiteSpace().isEmpty() ) + readRange( string.mid( 0, 7 ), new_ingredient.amount, new_ingredient.amount_offset ); + + //unit + TQString unit_str = string.mid( 8, 9 ).stripWhiteSpace(); + new_ingredient.units = Unit( unit_str, new_ingredient.amount ); + + //name and preparation method + new_ingredient.name = string.mid( 18, string.length() - 18 ).stripWhiteSpace(); + + //separate out the preparation method + TQString name_and_prep = new_ingredient.name; + int separator_index = name_and_prep.find( "," ); + if ( separator_index != -1 ) { + new_ingredient.name = name_and_prep.mid( 0, separator_index ).stripWhiteSpace(); + new_ingredient.prepMethodList = ElementList::split(",",name_and_prep.mid( separator_index + 1, name_and_prep.length() ).stripWhiteSpace() ); + } + + //header (if present) + new_ingredient.group = current_header; + + bool last_is_sub = is_sub; + if ( new_ingredient.prepMethodList.last().name == "or" ) { + new_ingredient.prepMethodList.pop_back(); + is_sub = true; + } + else + is_sub = false; + + if ( last_is_sub ) + ( *recipe.ingList.at( recipe.ingList.count() - 1 ) ).substitutes.append(new_ingredient); + else + recipe.ingList.append( new_ingredient ); +} + +void RezkonvImporter::loadIngredientHeader( const TQString &string, Recipe &/*recipe*/ ) +{ + TQString header = string; + header.remove( TQRegExp( "^=*" ) ).remove( TQRegExp( "=*$" ) ); + header = header.stripWhiteSpace(); + + kdDebug() << "found ingredient header: " << header << endl; + + current_header = header; +} + +void RezkonvImporter::loadInstructions( TQStringList::const_iterator &text_it, Recipe &recipe ) +{ + TQString instr; + TQRegExp rx_title( "^:{0,1}\\s*O-Titel\\s*:" ); + TQString line; + while ( text_it != m_end_it ) { + line = *text_it; + + //titles longer than the line width are rewritten here + if ( line.contains( rx_title ) ) { + line.remove( rx_title ); + recipe.title = line.stripWhiteSpace(); + + TQRegExp rx_line_cont( ":\\s*>{0,1}\\s*:" ); + while ( ( line = *text_it ).contains( rx_line_cont ) ) { + line.remove( rx_line_cont ); + recipe.title += line; + + text_it++; + } + kdDebug() << "Found long title: " << recipe.title << endl; + } + else { + if ( line.isEmpty() ) + instr += "\n\n"; + + instr += line; + } + + text_it++; + } + + recipe.instructions = instr; +} + +void RezkonvImporter::loadReferences( TQStringList::const_iterator &text_it, Recipe &recipe ) +{ + kdDebug() << "Found source header" << endl; + + while ( text_it != m_end_it ) { + text_it++; + TQRegExp rx_line_begin( "^\\s*-{0,2}\\s*" ); + + TQRegExp rx_creation_date = TQRegExp( "^\\s*-{0,2}\\s*Erfasst \\*RK\\*", false ); + if ( ( *text_it ).contains( rx_creation_date ) ) // date followed by typist + { + TQString date = *text_it; + date.remove( rx_creation_date ).remove( TQRegExp( " von\\s*$" ) ); + + // Date is given as DD.MM.YY + TQString s = date.section( ".", 0, 0 ); + int day = s.toInt(); + + s = date.section( ".", 1, 1 ); + int month = s.toInt(); + + s = date.section( ".", 2, 2 ); + int year = s.toInt(); + year += 1900; + if ( year < 1970 ) + year += 100; //we'll assume nothing has been created before 1970 (y2k issues :p) + + recipe.ctime = TQDate(year,month,day); + + #if 0 + //typist + text_it++; + TQString typist = = *text_it; + typist.remove( rx_line_begin ); + #endif + + } + else //everything else is an author + { + if ( ( *text_it ).contains( rx_line_begin ) ) { + TQString author = *text_it; + author.remove( rx_line_begin ); + + recipe.authorList.append( Element( author ) ); + } + else + break; + } + } +} + +void RezkonvImporter::readRange( const TQString &range_str, double &amount, double &amount_offset ) +{ + TQString from = range_str.section( '-', 0, 0 ); + TQString to = range_str.section( '-', 1, 1 ); + + amount = MixedNumber::fromString( from ).toDouble(); + + if ( !to.stripWhiteSpace().isEmpty() ) + amount_offset = MixedNumber::fromString( to ).toDouble() - amount; +} diff --git a/src/importers/rezkonvimporter.h b/src/importers/rezkonvimporter.h new file mode 100644 index 0000000..7f22255 --- /dev/null +++ b/src/importers/rezkonvimporter.h @@ -0,0 +1,42 @@ +/*************************************************************************** +* Copyright (C) 2003 by * +* Jason Kivlighn ([email protected]) * +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +***************************************************************************/ + +#ifndef REZKONVIMPORTER_H +#define REZKONVIMPORTER_H + +#include <tqstringlist.h> + +#include "baseimporter.h" +#include "datablocks/recipe.h" + +class RezkonvImporter : public BaseImporter +{ +public: + RezkonvImporter(); + ~RezkonvImporter(); + +protected: + void parseFile( const TQString &filename ); + +private: + void loadIngredient( const TQString &line, Recipe & recipe, bool &is_sub ); + void loadIngredientHeader( const TQString &line, Recipe & recipe ); + void loadInstructions( TQStringList::const_iterator &raw_text, Recipe & recipe ); + void loadReferences( TQStringList::const_iterator &raw_text, Recipe & recipe ); + + void readRange( const TQString &, double &amount, double &amount_offset ); + void readRecipe( const TQStringList &raw_text ); + + TQStringList::const_iterator m_end_it; + + TQString current_header; +}; + +#endif //REZKONVIMPORTER_H |